Lebenszyklen von Status in Compose

In Jetpack Compose speichern zusammensetzbare Funktionen den Status häufig mit der Funktion remember. Gespeicherte Werte können bei Neuzusammensetzungen wiederverwendet werden, wie unter Status und Jetpack Compose erläutert.

remember dient zwar als Tool, um Werte über Neuzusammensetzungen hinweg beizubehalten, aber der Status muss oft über die Lebensdauer einer Zusammensetzung hinaus bestehen bleiben. Auf dieser Seite wird der Unterschied zwischen den APIs remember, retain, rememberSaveable und rememberSerializable erläutert. Außerdem wird erklärt, wann welche API ausgewählt werden sollte und welche Best Practices für die Verwaltung von gespeicherten und beibehaltenen Werten in Compose gelten.

Wählen Sie die richtige Lebensdauer aus.

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

Werte bleiben bei Neuzusammensetzungen erhalten?

✅

✅

✅

Werte bleiben bei der Neuerstellung von Aktivitäten erhalten?

❌

✅

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

✅

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

Bleiben Werte nach der Prozessbeendigung erhalten?

❌

❌

✅

Unterstützte Datentypen

Alle

Darf nicht auf Objekte verweisen, die bei der Zerstörung der Aktivität verloren gehen würden

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-Genauigkeit wiederhergestellt werden kann
  • Caches
  • Langlebige oder „Manager“-Objekte
  • Nutzereingabe
  • Zustand, der von der App nicht wiederhergestellt werden kann, einschließlich Textfeldeingaben, Scrollstatus, Ein/Aus-Schaltflächen usw.

remember

remember ist die gängigste Methode, um den Status in Compose zu speichern. Wenn remember zum ersten Mal aufgerufen wird, wird die angegebene Berechnung ausgeführt und gespeichert. Das bedeutet, dass sie von Compose für die zukünftige Wiederverwendung durch die zusammensetzbare Funktion gespeichert wird. Wenn ein Compose-Element neu zusammengesetzt wird, wird der Code noch einmal ausgeführt. Bei Aufrufen von remember werden jedoch die Werte aus der vorherigen Zusammensetzung zurückgegeben, anstatt die Berechnung noch einmal auszuführen.

Jede Instanz einer zusammensetzbaren Funktion hat einen eigenen Satz gespeicherter Werte, was als positionale Memoisierung bezeichnet wird. Wenn gespeicherte Werte für die Verwendung in Neuzusammensetzungen memoriert werden, sind sie an ihre Position in der Kompositionshierarchie gebunden. Wenn ein Composable an verschiedenen Stellen verwendet wird, hat jede Instanz in der Kompositionshierarchie einen eigenen Satz gespeicherter Werte.

Wenn ein gespeicherter Wert nicht mehr verwendet wird, wird er vergessen und sein Eintrag wird verworfen. Gespeicherte Werte werden vergessen, wenn sie aus der Kompositionshierarchie entfernt werden. Das gilt auch, wenn ein Wert entfernt und wieder hinzugefügt wird, um ihn an eine andere Stelle zu verschieben, ohne das key- oder MovableContent-Composable zu verwenden, oder wenn er 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. Sie eignet sich am besten für:

  • Erstellen interner Zustandsobjekte, z. B. Scrollposition oder Animationsstatus
  • Vermeiden, dass bei jeder Neuzusammensetzung teure Objekte neu erstellt werden

Folgendes sollten Sie jedoch vermeiden:

  • Speichern von Nutzereingaben mit remember, da gespeicherte Objekte bei Änderungen der Aktivitätskonfiguration und bei vom System initiierten Prozessbeendigungen vergessen werden.

rememberSaveable und rememberSerializable

rememberSaveable und rememberSerializable bauen auf remember auf. Sie haben die längste Lebensdauer der in diesem Leitfaden beschriebenen Memofunktionen. Zusätzlich zur positionsbezogenen Memoisierung von Objekten über Neuzusammensetzungen hinweg können auch Werte gespeichert werden, damit sie bei der Neuerstellung von Aktivitäten wiederhergestellt werden können, einschließlich bei Konfigurationsänderungen und Prozessbeendigungen (wenn das System den Prozess Ihrer App beendet, während sie sich im Hintergrund befindet, normalerweise entweder um Speicher für Apps im Vordergrund freizugeben oder wenn der Nutzer Ihrer App Berechtigungen entzieht, während sie ausgeführt wird).

rememberSerializable funktioniert genauso wie rememberSaveable, unterstützt aber automatisch die Persistenz komplexer Typen, die mit der Bibliothek kotlinx.serialization serialisierbar sind. Wählen Sie rememberSerializable aus, wenn Ihr Typ mit @Serializable gekennzeichnet ist (oder werden kann), und in allen anderen Fällen rememberSaveable.

Dadurch eignen sich sowohl rememberSaveable als auch rememberSerializable perfekt zum Speichern von Zuständen, die mit Nutzereingaben verknüpft sind, einschließlich Textfeldeingaben, Scrollpositionen, Ein/Aus-Zuständen usw. Sie sollten diesen Zustand speichern, damit der Nutzer nie den Überblick verliert. Im Allgemeinen sollten Sie rememberSaveable oder rememberSerializable verwenden, um alle Zustände zu speichern, die Ihre Anwendung nicht aus einer anderen persistenten Datenquelle wie einer Datenbank abrufen kann.

Beachten Sie, dass rememberSaveable und rememberSerializable ihre gespeicherten Werte serialisieren und in einem Bundle speichern. Das hat zwei Folgen:

  • Werte, die Sie memoizen, müssen durch einen oder mehrere der folgenden Datentypen darstellbar sein: Primitive (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 die Komposition zuvor verwendet hat.

Wenn Sie komplexere Datentypen ohne kotlinx.serialization speichern möchten, können Sie einen benutzerdefinierten Saver implementieren, um Ihr Objekt in unterstützte Datentypen zu serialisieren und zu deserialisieren. Beachten Sie, dass Compose gängige Datentypen wie State, List, Map, Set usw. standardmäßig erkennt und automatisch in unterstützte Typen konvertiert. Das folgende Beispiel zeigt ein Saver-Objekt für eine Size-Klasse. Dazu werden alle Attribute von Size mit listSaver in eine Liste gepackt.

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 in Bezug auf die Dauer der Memoisierung ihrer Werte zwischen remember und rememberSaveable/rememberSerializable. Der Name ist anders, weil beibehaltene Werte auch einen anderen Lebenszyklus haben als ihre gemerkten Gegenstücke.

Wenn ein Wert beibehalten wird, wird er sowohl positionell gespeichert 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 keine Prozessbeendigung. Wenn ein Wert nach der Neuerstellung der Kompositionshierarchie nicht verwendet wird, wird der beibehaltene Wert zurückgezogen (was in retain dem Vergessen entspricht).

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, Abläufe und große Objekte wie Bitmaps. Sie können beispielsweise retain verwenden, um einen Mediaplayer (z. B. ExoPlayer) zu verwalten und Unterbrechungen der Medienwiedergabe während 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 gegen ViewModel

Im Kern bieten sowohl retain als auch ViewModel ähnliche Funktionen in ihrer am häufigsten verwendeten Fähigkeit, Objektinstanzen über Konfigurationsänderungen hinweg beizubehalten. Die Entscheidung, ob Sie retain oder ViewModel verwenden, hängt von der Art des Werts ab, den Sie beibehalten möchten, wie er eingeschränkt werden soll und ob Sie zusätzliche Funktionen benötigen.

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

ViewModel enthält auch sofort einsatzbereite Integrationen für die Abhängigkeitsinjektion mit Dagger und Hilt, die Integration mit SavedState und die integrierte Coroutine-Unterstützung zum Starten von Hintergrundaufgaben. Dadurch ist ViewModel ein idealer Ort, um Hintergrundaufgaben und Netzwerkanfragen zu starten, mit anderen Datenquellen in Ihrem Projekt zu interagieren und optional geschäftskritischen UI-Zustand zu erfassen und beizubehalten, der sowohl bei Konfigurationsänderungen in ViewModel beibehalten werden als auch die Prozessbeendigung überstehen soll.

retain eignet sich am besten für Objekte, die auf bestimmte zusammensetzbare Instanzen beschränkt sind und nicht zwischen gleichgeordneten zusammensetzbaren Elementen wiederverwendet oder geteilt werden müssen. Während ViewModel ein guter Ort ist, um den UI-Status zu speichern und Hintergrundaufgaben auszuführen, ist retain ein guter Kandidat für das Speichern von Objekten für die UI-Infrastruktur wie Caches, Impressions-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 „ViewModel-ähnliche“ API zu erstellen. Obwohl die Unterstützung für Koroutinen und gespeicherte Zustände nicht standardmäßig angeboten wird, kann retain als Baustein für den Lebenszyklus solcher ViewModel-ähnlichen Objekte mit diesen Funktionen dienen. Die Einzelheiten zum Entwerfen einer solchen Komponente werden in diesem Leitfaden nicht behandelt.

retain

ViewModel

Scoping

Keine gemeinsamen Werte; jeder Wert wird an einem bestimmten Punkt in der Kompositionshierarchie beibehalten und mit diesem verknüpft. Wenn Sie denselben Typ an einem anderen Ort beibehalten, wird immer eine neue Instanz erstellt.

ViewModels sind Singletons innerhalb einer ViewModelStore.

Zerstörung

Beim dauerhaften Verlassen der Kompositionshierarchie

Wenn die 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

Anwendungsbeispiele

  • 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 Datenebenen in eine separate Klasse extrahieren, sowohl für die Codeorganisation als auch für Tests
  • Flow-Objekte in State-Objekte umwandeln und Sperrfunktionen aufrufen, die nicht durch Konfigurationsänderungen unterbrochen werden sollen
  • Zustände über große UI-Bereiche wie ganze Bildschirme hinweg teilen
  • Interoperabilität mit View

retain und rememberSaveable oder rememberSerializable kombinieren

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

Es ist möglich, retain und rememberSaveable oder rememberSerializable gleichzeitig zu verwenden. Die korrekte Kombination beider Lebenszyklen ist sehr komplex. Wir empfehlen, dieses Muster als Teil komplexerer und benutzerdefinierter Architekturmuster 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 verfolgt, und ein Speicher-Cache, der nicht auf die Festplatte geschrieben werden kann).
  • Der Status ist auf eine zusammensetzbare Funktion beschränkt und eignet sich nicht für die Singleton-Beschränkung oder die Lebensdauer von ViewModel.

Wenn alle 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 die Aktualisierung des Status 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 wird die Trennung von Verantwortlichkeiten und Speicher sehr explizit. Es ist beabsichtigt, dass Speicherdaten nicht durch Aufbewahrungsdaten manipuliert werden können, da so verhindert wird, dass ein Update der Speicherdaten versucht wird, 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).

Positionsbasierte Memoisierung und adaptive Layouts

Android-Apps können viele Formfaktoren unterstützen, darunter Smartphones, faltbare Smartphones, Tablets und Desktops. Anwendungen müssen häufig mithilfe von adaptiven Layouts zwischen diesen Formfaktoren wechseln. Beispielsweise kann eine App, die auf einem Tablet ausgeführt wird, eine zweispaltige Listen-Detailansicht anzeigen, aber auf einem kleineren Smartphone-Bildschirm zwischen einer Listen- und einer Detailseite wechseln.

Da gemerkte und beibehaltene Werte positionsbezogen gespeichert werden, werden sie nur wiederverwendet, wenn sie an derselben Stelle in der Kompositionshierarchie erscheinen. Wenn sich Ihre Layouts an verschiedene Formfaktoren anpassen, kann sich die Struktur Ihrer Kompositionshierarchie ändern und zu vergessenen Werten führen.

Bei Standardkomponenten 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, müssen Sie dafür sorgen, dass der Status nicht durch Layoutänderungen beeinflusst wird. Gehen Sie dazu so vor:

  • Achte darauf, dass zustandsorientierte Composables immer an derselben Stelle in der Kompositionshierarchie aufgerufen werden. Implementieren Sie adaptive Layouts, indem Sie die Layoutlogik ändern, anstatt Objekte in der Kompositionshierarchie zu verschieben.
  • Verwenden Sie MovableContent, um zustandsorientierte Composables ordnungsgemäß zu verschieben. MovableContent-Instanzen können gespeicherte und beibehaltene Werte von ihren alten an ihre neuen Speicherorte verschieben.

Factory-Funktionen merken

Compose-UIs bestehen zwar aus zusammensetzbaren Funktionen, aber für die Erstellung und Organisation einer Komposition sind viele Objekte erforderlich. Das häufigste Beispiel hierfür sind komplexe zusammensetzbare Objekte, die ihren eigenen Status definieren, z. B. LazyList, das ein LazyListState akzeptiert.

Wenn Sie Compose-fokussierte Objekte definieren, empfehlen wir, eine remember-Funktion zu erstellen, um das gewünschte Speicherverhalten zu definieren, einschließlich Lebensdauer und Schlüsseleingaben. So können Nutzer Ihres Status zuverlässig Instanzen in der Kompositionshierarchie erstellen, die wie erwartet beibehalten und ungültig gemacht werden. Beachten Sie beim Definieren einer zusammensetzbaren Factory-Funktion die folgenden Richtlinien:

  • Stellen Sie dem Funktionsnamen das Präfix remember voran. Wenn die Funktionsimplementierung davon abhängt, dass das Objekt retained ist, und die API nie so weiterentwickelt wird, dass sie auf einer anderen Variante von remember basiert, verwenden Sie stattdessen das Präfix retain.
  • Verwenden Sie rememberSaveable oder rememberSerializable, wenn die Zustandspersistenz ausgewählt ist und eine korrekte Saver-Implementierung geschrieben werden kann.
  • Vermeiden Sie Nebenwirkungen oder die Initialisierung von Werten basierend auf CompositionLocals, die für die Nutzung möglicherweise nicht relevant sind. Denken Sie daran, dass der Ort, an dem Ihr Zustand erstellt wird, nicht unbedingt der Ort ist, 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) }
    )
}