Zustands-Callbacks mit RememberObserver und RetainObserver erstellen

In Jetpack Compose kann ein Objekt RememberObserver implementieren, um Callbacks zu empfangen, wenn es mit remember verwendet wird. So kann es erkennen, wann es in der Kompositionshierarchie gespeichert wird und wann nicht mehr. Ebenso können Sie RetainObserver verwenden, um Informationen zum Status eines Objekts zu erhalten, das mit retain verwendet wird.

Für Objekte, die diese Informationen zum Lebenszyklus aus der Kompositionshierarchie verwenden, empfehlen wir einige Best Practices, um sicherzustellen, dass sich Ihre Objekte auf der Plattform gut verhalten und vor Missbrauch geschützt sind. Verwenden Sie insbesondere die Callbacks onRemembered (oder onRetained), um Aufgaben zu starten, anstatt den Konstruktor zu verwenden. Brechen Sie alle Aufgaben ab, wenn Objekte nicht mehr gespeichert oder beibehalten werden, und vermeiden Sie die Weitergabe von Implementierungen von RememberObserver und RetainObserver, um versehentliche Aufrufe zu vermeiden. Im nächsten Abschnitt werden diese Empfehlungen genauer erläutert.

Initialisierung und Bereinigung mit RememberObserver und RetainObserver

Im Leitfaden „In Compose denken“ wird das mentale Modell hinter der Komposition beschrieben. Bei der Arbeit mit RememberObserver und RetainObserver sind zwei Verhaltensweisen der Komposition zu beachten:

  • Die Neukomposition ist optimistisch und kann abgebrochen werden
  • Alle zusammensetzbaren Funktionen sollten keine Nebeneffekte haben.

Nebeneffekte der Initialisierung während onRemembered oder onRetained ausführen, nicht während der Konstruktion

Wenn Objekte in Erinnerung behalten oder beibehalten werden, wird das Berechnungs-Lambda als Teil der Komposition ausgeführt. Aus denselben Gründen, aus denen Sie während der Komposition keine Nebeneffekte ausführen oder keine Coroutine starten würden, sollten Sie auch keine Nebeneffekte im Berechnungs-Lambda ausführen, das an remember, retain und deren Varianten übergeben wird. Dies gilt auch für den Konstruktor für die gespeicherten oder beibehaltenen Objekte.

Prüfen Sie stattdessen bei der Implementierung von RememberObserver oder RetainObserver, ob alle Effekte und gestarteten Jobs im onRemembered-Callback gesendet werden. Das Timing ist dasselbe wie bei den SideEffect-APIs. Außerdem wird dadurch sichergestellt, dass diese Effekte nur ausgeführt werden, wenn die Komposition angewendet wird. So werden verwaiste Jobs und Speicherlecks verhindert, wenn eine Neukomposition abgebrochen oder verzögert wird.

class MyComposeObject : RememberObserver {
    private val job = Job()
    private val coroutineScope = CoroutineScope(Dispatchers.Main + job)

    init {
        // Not recommended: This will cause work to begin during composition instead of
        // with other effects. Move this into onRemembered().
        coroutineScope.launch { loadData() }
    }

    override fun onRemembered() {
        // Recommended: Move any cancellable or effect-driven work into the onRemembered
        // callback. If implementing RetainObserver, this should go in onRetained.
        coroutineScope.launch { loadData() }
    }

    private suspend fun loadData() { /* ... */ }

    // ...
}

Entfernen bei Vergessen, Außerbetriebnahme oder Aufgabe

Um Ressourcenlecks oder verwaiste Hintergrundjobs zu vermeiden, müssen auch gespeicherte Objekte verworfen werden. Für Objekte, die RememberObserver implementieren, bedeutet das, dass für alles, was in onRemembered initialisiert wird, ein entsprechender Release-Aufruf in onForgotten vorhanden sein muss.

Da die Zusammensetzung abgebrochen werden kann, müssen Objekte, die RememberObserver implementieren, auch dann aufgeräumt werden, wenn sie in Kompositionen nicht mehr verwendet werden. Ein Objekt wird verworfen, wenn es von remember in einer Komposition zurückgegeben wird, die abgebrochen wird oder fehlschlägt. Das passiert am häufigsten bei der Verwendung von PausableComposition und kann auch bei der Verwendung von Hot-Reload mit den Composable-Vorschautools von Android Studio auftreten.

Wenn ein gemerktes Objekt aufgegeben wird, wird nur der Aufruf von onAbandoned ausgeführt (und kein Aufruf von onRemembered). Um die Methode „abandon“ zu implementieren, müssen Sie alle Elemente verwerfen, die zwischen der Initialisierung des Objekts und dem Zeitpunkt erstellt wurden, zu dem das Objekt den onRemembered-Rückruf erhalten hätte.

class MyComposeObject : RememberObserver {
    private val job = Job()
    private val coroutineScope = CoroutineScope(Dispatchers.Main + job)

    // ...

    override fun onForgotten() {
        // Cancel work launched from onRemembered. If implementing RetainObserver, onRetired
        // should cancel work launched from onRetained.
        job.cancel()
    }

    override fun onAbandoned() {
        // If any work was launched by the constructor as part of remembering the object,
        // you must cancel that work in this callback. For work done as part of the construction
        // during retain, this code should will appear in onUnused.
        job.cancel()
    }
}

RememberObserver- und RetainObserver-Implementierungen vertraulich behandeln

Seien Sie beim Schreiben öffentlicher APIs vorsichtig, wenn Sie RememberObserver und RetainObserver erweitern, um öffentlich zurückgegebene Klassen zu erstellen. Ein Nutzer erinnert sich möglicherweise nicht an Ihr Objekt, wenn Sie es erwarten, oder erinnert sich anders daran, als Sie es beabsichtigt haben. Daher empfehlen wir, keine Konstruktoren oder Factory-Funktionen für Objekte verfügbar zu machen, die RememberObserver oder RetainObserver implementieren. Dies hängt vom Laufzeittyp einer Klasse ab, nicht vom deklarierten Typ. Wenn ein Objekt, das RememberObserver oder RetainObserver implementiert, in Any umgewandelt wird, erhält es weiterhin Callbacks.

Nicht empfohlen:

abstract class MyManager

// Not Recommended: Exposing a public constructor (even implicitly) for an object implementing
// RememberObserver can cause unexpected invocations if it is remembered multiple times.
class MyComposeManager : MyManager(), RememberObserver { ... }

// Not Recommended: The return type may be an implementation of RememberObserver and should be
// remembered explicitly.
fun createFoo(): MyManager = MyComposeManager()

Empfohlen:

abstract class MyManager

class MyComposeManager : MyManager() {
    // Callers that construct this object must manually call initialize and teardown
    fun initialize() { /*...*/ }
    fun teardown() { /*...*/ }
}

@Composable
fun rememberMyManager(): MyManager {
    // Protect the RememberObserver implementation by never exposing it outside the library
    return remember {
        object : RememberObserver {
            val manager = MyComposeManager()
            override fun onRemembered() = manager.initialize()
            override fun onForgotten() = manager.teardown()
            override fun onAbandoned() { /* Nothing to do if manager hasn't initialized */ }
        }
    }.manager
}

Hinweise zum Merken von Objekten

Zusätzlich zu den vorherigen Empfehlungen zu RememberObserver und RetainObserver empfehlen wir, darauf zu achten, dass Objekte nicht versehentlich neu gespeichert werden. Das ist sowohl aus Leistungs- als auch aus Korrektheitsgründen wichtig. In den folgenden Abschnitten werden bestimmte Szenarien für das erneute Erinnern genauer beschrieben und es wird erläutert, warum sie vermieden werden sollten.

Objekte nur einmal merken

Das erneute Erinnern an ein Objekt kann gefährlich sein. Im besten Fall verschwenden Sie Ressourcen, weil Sie sich an einen Wert erinnern, an den Sie sich bereits erinnern. Wenn ein Objekt jedoch RememberObserver implementiert und unerwartet zweimal gespeichert wird, erhält es mehr Callbacks als erwartet. Das kann zu Problemen führen, da die Logik für onRemembered und onForgotten zweimal ausgeführt wird und die meisten Implementierungen von RememberObserver diesen Fall nicht unterstützen. Wenn ein zweiter Remember-Aufruf in einem anderen Bereich mit einer anderen Lebensdauer als der ursprüngliche remember erfolgt, geben viele Implementierungen von RememberObserver.onForgotten das Objekt frei, bevor es vollständig verwendet wurde.

val first: RememberObserver = rememberFoo()

// Not Recommended: Re-remembered `Foo` now gets double callbacks
val second = remember { first }

Diese Empfehlung gilt nicht für Objekte, die transitiv wieder in Erinnerung gerufen werden (z. B. Objekte, die ein anderes Objekt in Erinnerung rufen). Es ist üblich, Code zu schreiben, der so aussieht. Das ist zulässig, da ein anderes Objekt gespeichert wird und daher keine unerwartete Verdoppelung des Rückrufs erfolgt.

val foo: Foo = rememberFoo()

// Acceptable:
val bar: Bar = remember { Bar(foo) }

// Recommended key usage:
val barWithKey: Bar = remember(foo) { Bar(foo) }

Annehmen, dass Funktionsargumente bereits gespeichert sind

Eine Funktion sollte sich keine ihrer Parameter merken, da dies zu doppelten Callback-Aufrufen für RememberObserver führen kann und unnötig ist. Wenn ein Eingabeparameter gespeichert werden muss, prüfen Sie, ob er RememberObserver nicht implementiert, oder fordern Sie Anrufer auf, sich ihr Argument zu merken.

@Composable
fun MyComposable(
    parameter: Foo
) {
    // Not Recommended: Input should be remembered by the caller.
    val rememberedParameter = remember { parameter }
}

Das gilt nicht für transitiv gespeicherte Objekte. Wenn Sie sich ein Objekt merken möchten, das aus den Argumenten einer Funktion abgeleitet wurde, sollten Sie es als einen der Schlüssel für remember angeben:

@Composable
fun MyComposable(
    parameter: Foo
) {
    // Acceptable:
    val derivedValue = remember { Bar(parameter) }

    // Also Acceptable:
    val derivedValueWithKey = remember(parameter) { Bar(parameter) }
}

Ein Objekt, an das sich bereits erinnert wird, nicht beibehalten

Ähnlich wie beim erneuten Merken eines Objekts sollten Sie vermeiden, ein gemerktes Objekt beizubehalten, um seine Lebensdauer zu verlängern. Dies ist eine Folge der Empfehlung unter Statuslebenszyklen: retain sollte nicht für Objekte verwendet werden, deren Lebenszyklus nicht mit dem Lebenszyklus von Angeboten zur Kundenbindung übereinstimmt. Da remembered-Objekte eine kürzere Lebensdauer als retained-Objekte haben, sollten Sie kein gespeichertes Objekt beibehalten. Behalten Sie das Objekt stattdessen lieber am ursprünglichen Standort bei, anstatt es sich zu merken.