Lebenszyklen von Status in Compose

In Jetpack Compose wird der Status in zusammensetzbaren Funktionen häufig mit der Funktion remember gespeichert. Werte, die gespeichert werden, können bei Recompositions wiederverwendet werden, wie unter State und Jetpack Compose beschrieben.

remember ist zwar ein Tool, mit dem Werte über Recompositionen hinweg beibehalten werden können, der Status muss jedoch oft über die Lebensdauer einer Komposition hinaus bestehen bleiben. Auf dieser Seite wird der Unterschied zwischen den APIs remember, retain, rememberSaveable und rememberSerializable erläutert. Außerdem wird beschrieben, wann welche API verwendet werden sollte und welche Best Practices für die Verwaltung von gespeicherten und beibehaltenen Werten in Compose gelten.

Die richtige Lebensdauer auswählen

In Compose gibt es mehrere Funktionen, mit denen Sie den Status über Kompositionen hinweg und darüber hinaus beibehalten können: remember, retain, rememberSaveable und rememberSerializable. Diese Funktionen unterscheiden sich in ihrer Lebensdauer und Semantik und eignen sich jeweils zum Speichern bestimmter Arten von Status. Die Unterschiede sind in der folgenden Tabelle aufgeführt:

remember

retain

rememberSaveable, rememberSerializable

Bleiben Werte bei Neuzusammenstellungen erhalten?

Bleiben Werte bei der Neuerstellung von Aktivitäten erhalten?

Es wird immer dieselbe (===) Instanz zurückgegeben.

Es wird ein entsprechendes (==) Objekt zurückgegeben, möglicherweise eine deserialisierte Kopie.

Bleiben Werte nach der Prozessbeendigung erhalten?

Unterstützte Datentypen

Alle

Es darf nicht auf Objekte verwiesen werden, die verloren gehen würden, wenn die Aktivität zerstört wird.

Muss serialisierbar sein
(entweder mit einem benutzerdefinierten Saver oder mit kotlinx.serialization)

Anwendungsfälle

  • Objekte, die auf die Komposition beschränkt sind
  • Konfigurationsobjekte für Composables
  • Zustand, der ohne Verlust der UI-Darstellung neu erstellt werden kann
  • Caches
  • Langlebige oder „Manager“-Objekte
  • Nutzereingabe
  • Status, der von der App nicht wiederhergestellt werden kann, z. B. Eingaben in Textfelder, Scrollstatus, Ein/Aus-Schalter usw.

remember

remember ist die gängigste Methode zum Speichern des Status in Compose. Wenn remember zum ersten Mal aufgerufen wird, wird die angegebene Berechnung ausgeführt und gespeichert. Das bedeutet, dass sie von Compose zur zukünftigen Wiederverwendung durch die Composable gespeichert wird. Wenn ein Composable neu zusammengesetzt wird, wird sein Code noch einmal ausgeführt. Alle Aufrufe von remember geben jedoch ihre Werte aus der vorherigen Zusammensetzung zurück, anstatt die Berechnung noch einmal auszuführen.

Jede Instanz einer zusammensetzbaren Funktion hat eine eigene Gruppe von gespeicherten Werten, die als positionelle Memoization bezeichnet werden. Wenn sich gemerkte Werte für die Verwendung bei Re-Compositions gemerkt werden, sind sie an ihre Position in der Kompositionshierarchie gebunden. Wenn ein Composable an verschiedenen Stellen verwendet wird, hat jede Instanz in der Kompositionshierarchie eigene gespeicherte Werte.

Wenn ein gespeicherter Wert nicht mehr verwendet wird, wird er vergessen und sein Datensatz wird verworfen. Gemerkte Werte gehen verloren, wenn sie aus der Kompositionshierarchie entfernt werden. Das gilt auch, wenn ein Wert entfernt und wieder hinzugefügt wird, um ihn an einen anderen Ort zu verschieben, ohne dass die zusammensetzbare Funktion key oder MovableContent verwendet wird, oder wenn sie mit anderen key-Parametern aufgerufen wird.

Von den verfügbaren Optionen hat remember die kürzeste Lebensdauer und vergisst Werte am frühesten von den vier auf dieser Seite beschriebenen Memoization-Funktionen. Daher eignet sie sich am besten für:

  • Erstellen von internen Statusobjekten, z. B. für die Scrollposition oder den Animationsstatus
  • Teure Neuerstellung von Objekten bei jeder Neuzusammensetzung vermeiden

Folgendes sollten Sie jedoch vermeiden:

  • Speichern Sie alle Nutzereingaben mit remember, da sich gemerkte Objekte bei Änderungen an der Aktivitätskonfiguration und bei vom System initiierten Prozessbeendigungen nicht mehr abrufen lassen.

rememberSaveable und rememberSerializable

rememberSaveable und rememberSerializable basieren auf remember. Sie haben die längste Lebensdauer der in diesem Leitfaden beschriebenen Memoization-Funktionen. Neben der positionsbezogenen Memoization von Objekten über Recompositions hinweg können auch Werte gespeichert werden, damit sie bei der Neuerstellung von Aktivitäten wiederhergestellt werden können. Das gilt auch bei Konfigurationsänderungen und Prozessbeendigung (wenn das System den Prozess Ihrer App im Hintergrund beendet, in der Regel entweder, um Speicher für Vordergrund-Apps freizugeben, oder wenn der Nutzer Berechtigungen für Ihre App widerruft, während sie ausgeführt wird).

rememberSerializable funktioniert genauso wie rememberSaveable, unterstützt aber automatisch das Speichern komplexer Typen, die mit der kotlinx.serialization-Bibliothek serialisiert werden können. Wählen Sie rememberSerializable aus, wenn Ihr Typ mit @Serializable gekennzeichnet ist oder werden kann, und rememberSaveable in allen anderen Fällen.

Daher eignen sich sowohl rememberSaveable als auch rememberSerializable hervorragend zum Speichern von Status, der mit der Nutzereingabe verknüpft ist, z. B. Textfeldeingaben, Scrollpositionen und Umschaltstatus. Sie sollten diesen Status speichern, damit der Nutzer nie den Überblick verliert. Im Allgemeinen sollten Sie rememberSaveable oder rememberSerializable verwenden, um alle Status zu speichern, die Ihre App nicht aus einer anderen persistenten Datenquelle wie einer Datenbank abrufen kann.

rememberSaveable und rememberSerializable speichern ihre zwischengespeicherten Werte, indem sie sie in ein Bundle serialisieren. Das hat zwei Folgen:

  • Die Werte, die Sie zwischenspeichern, müssen durch einen oder mehrere der folgenden Datentypen dargestellt werden können: Primitiven (einschließlich Int, Long, Float, Double), String oder Arrays eines dieser Typen.
  • Wenn ein gespeicherter Wert wiederhergestellt wird, ist er eine neue Instanz, die gleich (==), aber nicht dieselbe Referenz (===) ist, die in der Komposition zuvor verwendet wurde.

Wenn Sie komplexere Datentypen speichern möchten, ohne kotlinx.serialization zu verwenden, können Sie eine benutzerdefinierte Saver implementieren, um Ihr Objekt in unterstützte Datentypen zu serialisieren und zu deserialisieren. Compose unterstützt gängige Datentypen wie State, List, Map und Set standardmäßig und konvertiert diese automatisch in unterstützte Typen. Das folgende Beispiel zeigt ein Saver-Objekt für eine Size-Klasse. Sie wird implementiert, indem alle Eigenschaften von Size mithilfe von listSaver in eine Liste gepackt werden.

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

Die retain API liegt zwischen remember und rememberSaveable/rememberSerializable in Bezug auf die Dauer, für die die Werte gespeichert werden. Die Bezeichnung ist anders, weil beibehaltene Werte auch einen anderen Lebenszyklus haben als die entsprechenden gespeicherten Werte.

Wenn ein Wert beibehalten wird, wird er sowohl positionsbezogen zwischengespeichert als auch in einer sekundären Datenstruktur gespeichert, die eine separate Lebensdauer hat, die an die Lebensdauer der App gebunden ist. Ein beibehaltener Wert kann Konfigurationsänderungen überstehen, ohne serialisiert zu werden, aber nicht, wenn der Prozess beendet wird. Wenn ein Wert nach der Neuerstellung der Kompositionshierarchie nicht verwendet wird, wird der beibehaltene Wert eingestellt (das Äquivalent von retain für das Vergessen).

Im Gegenzug für diesen kürzeren Lebenszyklus als rememberSaveable kann „retain“ Werte beibehalten, die nicht serialisiert werden können, z. B. Lambda-Ausdrücke, Flows und große Objekte wie Bitmaps. Sie können beispielsweise retain verwenden, um einen Media-Player (z. B. ExoPlayer) zu verwalten und Unterbrechungen der Medienwiedergabe bei einer Konfigurationsänderung zu verhindern.

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retain im Vergleich zu ViewModel

Im Grunde bieten sowohl retain als auch ViewModel ähnliche Funktionen, insbesondere die Möglichkeit, Objektinstanzen bei Konfigurationsänderungen beizubehalten. Ob Sie retain oder ViewModel verwenden, hängt vom Typ des Werts ab, den Sie beibehalten möchten, vom Umfang und davon, ob Sie zusätzliche Funktionen benötigen.

ViewModels sind Objekte, die in der Regel die Kommunikation zwischen der UI- und der Datenschicht Ihrer App kapseln. Sie ermöglichen es Ihnen, Logik aus Ihren zusammensetzbaren Funktionen zu entfernen, was die Testbarkeit verbessert. ViewModels werden als Singletons in einem ViewModelStore verwaltet und haben eine andere Lebensdauer als beibehaltene Werte. Ein ViewModel bleibt aktiv, bis sein ViewModelStore zerstört wird. Beibehaltene Werte werden jedoch entfernt, wenn der Inhalt dauerhaft aus der Komposition entfernt wird. Bei einer Konfigurationsänderung wird ein beibehaltener Wert beispielsweise entfernt, wenn die UI-Hierarchie neu erstellt wird und der beibehaltene Wert nach der Neuerstellung der Komposition nicht verwendet wurde.

ViewModel bietet auch sofort einsatzbereite Integrationen für die Abhängigkeitsinjektion mit Dagger und Hilt, die Integration mit SavedState und integrierte Unterstützung für Coroutinen zum Starten von Hintergrundaufgaben. Daher ist ViewModel ein idealer Ort, um Hintergrundaufgaben und Netzwerkanfragen zu starten, mit anderen Datenquellen in Ihrem Projekt zu interagieren und optional unternehmenskritische UI-Zustände zu erfassen und beizubehalten, die sowohl bei Konfigurationsänderungen in der ViewModel als auch bei Prozessbeendigung erhalten bleiben sollen.

retain eignet sich am besten für Objekte, die auf bestimmte zusammensetzbare Instanzen beschränkt sind und nicht zwischen gleichgeordneten zusammensetzbaren Funktionen wiederverwendet oder geteilt werden müssen. ViewModel ist ein guter Ort, um den UI-Status zu speichern und Hintergrundaufgaben auszuführen. retain eignet sich gut zum Speichern von Objekten für die UI-Infrastruktur wie Caches, Impression-Tracking und Analysen, Abhängigkeiten von AndroidViews und anderen Objekten, die mit dem Android-Betriebssystem interagieren oder Drittanbieterbibliotheken wie Zahlungsabwickler oder Werbung verwalten.

Für fortgeschrittene Nutzer, die benutzerdefinierte App-Architekturmuster außerhalb der Empfehlungen für die moderne Android-App-Architektur entwerfen: retain kann auch verwendet werden, um eine interne API zu erstellen, die ViewModel ähnelt. Obwohl die Unterstützung für Coroutinen und den gespeicherten Status nicht sofort verfügbar ist, kann retain als Baustein für den Lebenszyklus solcher ViewModel-ähnlichen Elemente dienen, bei denen diese Funktionen integriert sind. Wie Sie eine solche Komponente entwerfen, wird in dieser Anleitung nicht behandelt.

retain

ViewModel

Bereich

Keine gemeinsamen Werte: Jeder Wert wird beibehalten und einem bestimmten Punkt in der Kompositionshierarchie zugeordnet. Wenn Sie denselben Typ an einem anderen Ort beibehalten, wird immer eine neue Instanz erstellt.

ViewModel sind Singletons in einem ViewModelStore.

Zerstörung

Wenn Sie die Kompositionshierarchie dauerhaft verlassen

Wenn der ViewModelStore gelöscht oder zerstört wird

Zusätzliche Funktionen

Kann Callbacks empfangen, wenn sich das Objekt in der Kompositionshierarchie befindet oder nicht

Integrierte coroutineScope, Unterstützung für SavedStateHandle, kann mit Hilt eingefügt werden

Eigentümer

RetainedValuesStore

ViewModelStore

Anwendungsfälle

  • UI-spezifische Werte lokal für einzelne zusammensetzbare Instanzen beibehalten
  • Impressions-Tracking, möglicherweise über RetainedEffect
  • Baustein zum Definieren einer benutzerdefinierten „ViewModel-ähnlichen“ Architekturkomponente
  • Interaktionen zwischen UI- und Datenschicht in eine separate Klasse extrahieren, sowohl zur Codeorganisation als auch zum Testen
  • Flow-Objekte in State-Objekte umwandeln und Suspend-Funktionen aufrufen, die nicht durch Konfigurationsänderungen unterbrochen werden sollten
  • Status über große UI-Bereiche wie ganze Bildschirme hinweg freigeben
  • Interoperabilität mit View

retain und rememberSaveable oder rememberSerializable kombinieren

Manchmal muss ein Objekt eine hybride Lebensdauer von retained und rememberSaveable oder rememberSerializable haben. Das kann ein Hinweis darauf sein, dass Ihr Objekt ein ViewModel sein sollte, das den gespeicherten Status wie in der Anleitung zum Modul „Gespeicherter Status für ViewModel“ beschrieben unterstützt.

Es ist möglich, retain und rememberSaveable oder rememberSerializable gleichzeitig zu verwenden. Die korrekte Kombination beider Lebenszyklen ist mit erheblichem Aufwand verbunden. Wir empfehlen, dieses Muster als Teil von komplexeren und benutzerdefinierten Architekturmustern zu verwenden, und nur, wenn alle folgenden Bedingungen erfüllt sind:

  • Sie definieren ein Objekt, das aus einer Mischung von Werten besteht, die beibehalten oder gespeichert werden müssen (z. B. ein Objekt, das eine Nutzereingabe erfasst, und ein In-Memory-Cache, der nicht auf die Festplatte geschrieben werden kann).
  • Der Status ist auf ein Composable beschränkt und eignet sich nicht für den Singleton-Bereich oder die Lebensdauer von ViewModel.

Wenn all diese Bedingungen erfüllt sind, empfehlen wir, die Klasse in drei Teile aufzuteilen: die gespeicherten Daten, die beibehaltenen Daten und ein „Mediator“-Objekt, das keinen eigenen Status hat und den Status entsprechend an die beibehaltenen und gespeicherten Objekte delegiert. Dieses Muster hat die folgende Form:

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

Durch die Trennung des Status nach Lebensdauer werden die Verantwortlichkeiten und die Speicherung sehr explizit getrennt. Es ist beabsichtigt, dass gespeicherte Daten nicht durch Beibehaltungsdaten manipuliert werden können. So wird verhindert, dass versucht wird, gespeicherte Daten zu aktualisieren, wenn das savedInstanceState-Bundle bereits erfasst wurde und nicht aktualisiert werden kann. Außerdem können Sie Szenarien für die Neuerstellung testen, indem Sie Ihre Konstruktoren testen, ohne Compose aufzurufen oder eine Neuerstellung der Aktivität zu simulieren.

Ein vollständiges Beispiel für die Implementierung dieses Musters finden Sie im vollständigen Beispiel (RetainAndSaveSample.kt).

Positionelle Memoization und adaptive Layouts

Android-Anwendungen können viele Formfaktoren unterstützen, darunter Smartphones, faltbare Geräte, Tablets und Desktop-Computer. Anwendungen müssen häufig mithilfe von adaptiven Layouts zwischen diesen Formfaktoren wechseln. Eine App, die auf einem Tablet ausgeführt wird, kann beispielsweise eine Liste mit zwei Spalten und eine Detailansicht anzeigen, aber auf einem kleineren Smartphone-Display zwischen einer Liste und einer Detailseite wechseln.

Da sich gemerkte und beibehaltene Werte positionsbezogen merken, werden sie nur wiederverwendet, wenn sie an derselben Stelle in der Kompositionshierarchie vorkommen. Wenn sich Ihre Layouts an verschiedene Formfaktoren anpassen, kann sich die Struktur Ihrer Kompositionshierarchie ändern und es kann zu vergessenen Werten kommen.

Bei sofort einsatzbereiten Komponenten wie ListDetailPaneScaffold und NavDisplay (aus Jetpack Navigation 3) ist das kein Problem und der Status bleibt bei Layoutänderungen erhalten. Bei benutzerdefinierten Komponenten, die sich an Formfaktoren anpassen, muss der Status durch Layoutänderungen unbeeinflusst bleiben. Gehen Sie dazu so vor:

  • Achten Sie darauf, dass zustandsbehaftete Composables immer an derselben Stelle in der Kompositionshierarchie aufgerufen werden. Implementieren Sie adaptive Layouts, indem Sie die Layoutlogik ändern, anstatt Objekte in der Kompositionshierarchie neu zu positionieren.
  • Mit MovableContent können Sie zustandsorientierte kombinierbare Funktionen ordnungsgemäß verschieben. Instanzen von MovableContent können gespeicherte und beibehaltene Werte von ihren alten an die neuen Speicherorte verschieben.

Werkseinstellungen wiederherstellen

Obwohl Compose-UIs aus zusammensetzbaren Funktionen bestehen, sind viele Objekte an der Erstellung und Organisation einer Komposition beteiligt. Das häufigste Beispiel dafür sind komplexe zusammensetzbare Objekte, die ihren eigenen Status definieren, z. B. LazyList, das ein LazyListState akzeptiert.

Wenn Sie Compose-orientierte Objekte definieren, empfehlen wir, eine remember-Funktion zu erstellen, um das gewünschte Verhalten beim Speichern zu definieren, einschließlich Lebensdauer und wichtigen Eingaben. So können Nutzer Ihres Status sicher Instanzen in der Kompositionshierarchie erstellen, die wie erwartet erhalten bleiben und ungültig gemacht werden. Beachten Sie beim Definieren einer zusammensetzbaren Factory-Funktion die folgenden Richtlinien:

  • Stellen Sie dem Funktionsnamen remember voran. Wenn die Funktionsimplementierung optional davon abhängt, dass das Objekt retained ist, und die API sich nie so weiterentwickeln wird, dass sie auf einer anderen Variante von remember basiert, verwenden Sie stattdessen das Präfix retain.
  • Verwende rememberSaveable oder rememberSerializable, wenn die Statuspersistenz ausgewählt ist und eine korrekte Saver-Implementierung möglich ist.
  • Vermeiden Sie Nebenwirkungen oder die Initialisierung von Werten basierend auf CompositionLocal, die für die Nutzung möglicherweise nicht relevant sind. Der Ort, an dem der Status erstellt wird, ist möglicherweise nicht der Ort, an dem er verwendet wird.

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}