State und Jetpack Compose

Der Status in einer App ist jeder Wert, der sich im Laufe der Zeit ändern kann. Das ist eine sehr allgemeine Definition, die alles von einer Room-Datenbank bis hin zu einer Variablen in einer Klasse umfasst.

Alle Android-Apps zeigen dem Nutzer den Status an. Einige Beispiele für den Status in Android-Apps:

  • Eine Snackbar, die angezeigt wird, wenn keine Netzwerkverbindung hergestellt werden kann.
  • Ein Blogpost und die zugehörigen Kommentare.
  • Wellenanimationen auf Schaltflächen, die abgespielt werden, wenn ein Nutzer darauf klickt.
  • Sticker, die ein Nutzer auf ein Bild zeichnen kann.

Mit Jetpack Compose können Sie explizit angeben, wo und wie Sie den Status in einer Android-App speichern und verwenden. In diesem Leitfaden geht es um die Verbindung zwischen Status und Composables sowie um die APIs, die Jetpack Compose bietet, um einfacher mit dem Status zu arbeiten.

Zustand und Zusammensetzung

Compose ist deklarativ. Die einzige Möglichkeit, es zu aktualisieren, besteht darin, dasselbe Composable mit neuen Argumenten aufzurufen. Diese Argumente stellen den UI-Status dar. Jedes Mal, wenn ein Status aktualisiert wird, findet eine Neuzusammensetzung statt. Daher werden Dinge wie TextField nicht automatisch aktualisiert wie in imperativen XML-basierten Ansichten. Ein Composable muss explizit über den neuen Status informiert werden, damit es entsprechend aktualisiert wird.

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

Wenn Sie diesen Code ausführen und versuchen, Text einzugeben, werden Sie feststellen, dass nichts passiert. Das liegt daran, dass TextField nicht automatisch aktualisiert wird, sondern nur, wenn sich der Parameter value ändert. Das liegt daran, wie Komposition und Neukomposition in Compose funktionieren.

Weitere Informationen zur ersten Komposition und zur Neukomposition finden Sie unter Thinking in Compose.

Status in komponierbaren Funktionen

Composable-Funktionen können die remember-API verwenden, um ein Objekt im Arbeitsspeicher zu speichern. Ein von remember berechneter Wert wird während der ersten Komposition in der Komposition gespeichert und der gespeicherte Wert wird während der Neukomposition zurückgegeben. remember kann zum Speichern von veränderlichen und unveränderlichen Objekten verwendet werden.

mutableStateOf erstellt ein Observable MutableState<T>, das ein in die Compose-Laufzeit integrierter Observable-Typ ist.

interface MutableState<T> : State<T> {
    override var value: T
}

Alle Änderungen an value führen zu einer Neuzusammenstellung aller zusammensetzbaren Funktionen, die value lesen.

Es gibt drei Möglichkeiten, ein MutableState-Objekt in einer Composable-Funktion zu deklarieren:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

Diese Deklarationen sind gleichwertig und dienen als syntaktischer Zucker für verschiedene Verwendungszwecke von Status. Sie sollten die Option auswählen, die den am einfachsten zu lesenden Code in der Composable-Funktion erzeugt, die Sie schreiben.

Für die by-Delegat-Syntax sind die folgenden Importe erforderlich:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Sie können den gespeicherten Wert als Parameter für andere Composables oder sogar als Logik in Anweisungen verwenden, um zu ändern, welche Composables angezeigt werden. Wenn Sie beispielsweise die Begrüßung nicht anzeigen möchten, wenn der Name leer ist, verwenden Sie den Status in einer if-Anweisung:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

Mit remember können Sie den Status bei Recompositionen beibehalten, aber nicht bei Konfigurationsänderungen. Dazu müssen Sie rememberSaveable verwenden. rememberSaveable speichert automatisch alle Werte, die in einem Bundle gespeichert werden können. Für andere Werte können Sie ein benutzerdefiniertes Saver-Objekt übergeben.

Andere unterstützte Arten von Status

In Compose müssen Sie MutableState<T> nicht verwenden, um den Status zu speichern. Es werden auch andere beobachtbare Typen unterstützt. Bevor Sie einen anderen beobachtbaren Typ in Compose lesen, müssen Sie ihn in einen State<T> konvertieren, damit Composables automatisch neu zusammengesetzt werden können, wenn sich der Status ändert.

Compose bietet Funktionen zum Erstellen von State<T> aus gängigen beobachtbaren Typen, die in Android-Apps verwendet werden. Bevor Sie diese Integrationen verwenden, müssen Sie die entsprechenden Artefakte wie unten beschrieben hinzufügen:

  • Flow: collectAsStateWithLifecycle()

    Mit collectAsStateWithLifecycle() werden Werte aus einem Flow auf lebenszyklusbewusste Weise erfasst, sodass Ihre App Ressourcen sparen kann. Sie stellt den zuletzt ausgegebenen Wert aus dem Compose-State dar. Diese API ist die empfohlene Methode zum Erfassen von Abläufen in Android-Apps.

    Die folgende dependency ist in der Datei build.gradle erforderlich (sie sollte 2.6.0-beta01 oder höher sein):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow: collectAsState()

    collectAsState ähnelt collectAsStateWithLifecycle, da auch hier Werte aus einem Flow erfasst und in Compose State umgewandelt werden.

    Verwenden Sie collectAsState für plattformunabhängigen Code anstelle von collectAsStateWithLifecycle, das nur für Android gilt.

    Für collectAsState sind keine zusätzlichen Abhängigkeiten erforderlich, da es in compose-runtime verfügbar ist.

  • LiveData: observeAsState()

    observeAsState() beginnt mit der Beobachtung von LiveData und stellt die Werte über State dar.

    Die folgende Abhängigkeit ist in der Datei build.gradle erforderlich:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.8.1"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.8.1"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.8.1"
}

Zustandsorientiert im Vergleich zu zustandslos

Ein Composable, das remember zum Speichern eines Objekts verwendet, erstellt einen internen Status und macht das Composable zustandsbehaftet. HelloContent ist ein Beispiel für eine zustandsbehaftete Composable, da sie ihren name-Zustand intern speichert und ändert. Das kann in Situationen nützlich sein, in denen ein Aufrufer den Status nicht steuern muss und ihn verwenden kann, ohne den Status selbst verwalten zu müssen. Composables mit internem Status sind jedoch in der Regel weniger wiederverwendbar und schwieriger zu testen.

Eine zustandslose Composable ist eine Composable, die keinen Status enthält. Eine einfache Möglichkeit, Zustandsfreiheit zu erreichen, ist die Verwendung von State Hoisting.

Wenn Sie wiederverwendbare Composables entwickeln, möchten Sie oft sowohl eine zustandsorientierte als auch eine zustandslose Version desselben Composables bereitstellen. Die zustandsorientierte Version ist praktisch für Aufrufer, die sich nicht für den Zustand interessieren, und die zustandslose Version ist für Aufrufer erforderlich, die den Zustand steuern oder anheben müssen.

State Hoisting

State Hoisting in Compose ist ein Muster, bei dem der Status an den Aufrufer eines Composables übergeben wird, um das Composable zustandslos zu machen. Das allgemeine Muster für das State Hoisting in Jetpack Compose besteht darin, die Statusvariable durch zwei Parameter zu ersetzen:

  • value: T:der aktuelle anzuzeigende Wert
  • onValueChange: (T) -> Unit:Ein Ereignis, das eine Änderung des Werts anfordert, wobei T der vorgeschlagene neue Wert ist.

Sie sind jedoch nicht auf onValueChange beschränkt. Wenn spezifischere Ereignisse für die Composable infrage kommen, sollten Sie sie mit Lambdas definieren.

Der so hochgestufte Status hat einige wichtige Eigenschaften:

  • Single Source of Truth:Da wir den Status verschieben, anstatt ihn zu duplizieren, gibt es nur eine einzige Quelle der Wahrheit. So lassen sich Fehler vermeiden.
  • Gekapselt:Nur zustandsorientierte Composables können ihren Zustand ändern. Sie ist vollständig intern.
  • Freigabefähig:Der angehobene Status kann für mehrere Composables freigegeben werden. Wenn Sie name in einem anderen Composable lesen möchten, ist das durch Hoisting möglich.
  • Abfangbar:Aufrufer der zustandslosen Composables können Ereignisse ignorieren oder ändern, bevor sie den Status ändern.
  • Entkoppelt:Der Status für die zustandslosen Composables kann überall gespeichert werden. So ist es jetzt beispielsweise möglich, name in ein ViewModel zu verschieben.

Im Beispiel extrahieren Sie name und onValueChange aus HelloContent und verschieben sie im Baum nach oben zu einer HelloScreen-Composable, die HelloContent aufruft.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

Wenn Sie den Status aus HelloContent herausziehen, ist es einfacher, die Composable zu verstehen, sie in verschiedenen Situationen wiederzuverwenden und zu testen. HelloContent ist unabhängig davon, wie der Status gespeichert wird. Durch die Entkopplung müssen Sie die Implementierung von HelloContent nicht ändern, wenn Sie HelloScreen ändern oder ersetzen.

Das Muster, bei dem der Status sinkt und die Ereignisse steigen, wird als unidirektionaler Datenfluss bezeichnet. In diesem Fall sinkt der Status von HelloScreen auf HelloContent und die Ereignisse steigen von HelloContent auf HelloScreen. Durch die Verwendung eines unidirektionalen Datenflusses können Sie Composables, die den Status in der Benutzeroberfläche anzeigen, von den Teilen Ihrer App entkoppeln, in denen der Status gespeichert und geändert wird.

Weitere Informationen finden Sie auf der Seite Where to hoist state.

Zustand in Compose wiederherstellen

Die rememberSaveable API verhält sich ähnlich wie remember, da der Status über Re-Compositions und auch über die Neuerstellung von Aktivitäten oder Prozessen mithilfe des Mechanismus für den gespeicherten Instanzstatus hinweg beibehalten wird. Das passiert beispielsweise, wenn der Bildschirm gedreht wird.

Möglichkeiten zum Speichern des Status

Alle Datentypen, die Bundle hinzugefügt werden, werden automatisch gespeichert. Wenn Sie etwas speichern möchten, das nicht in die Bundle aufgenommen werden kann, haben Sie mehrere Möglichkeiten.

In Pakete aufteilen

Die einfachste Lösung ist, dem Objekt die Annotation @Parcelize hinzuzufügen. Das Objekt wird in ein Paket umgewandelt und kann gebündelt werden. Mit diesem Code wird beispielsweise ein Parcelable-Datentyp City erstellt und im Status gespeichert.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

Wenn @Parcelize aus irgendeinem Grund nicht geeignet ist, können Sie mit mapSaver eine eigene Regel zum Konvertieren eines Objekts in eine Reihe von Werten definieren, die das System in Bundle speichern kann.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

Um die Schlüssel für die Karte nicht definieren zu müssen, können Sie auch listSaver verwenden und die zugehörigen Indexe als Schlüssel nutzen:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

State Holder in Compose

Einfaches State Hoisting kann in den zusammensetzbaren Funktionen selbst verwaltet werden. Wenn jedoch die Menge des zu verfolgenden Status zunimmt oder die Logik für die Ausführung in zusammensetzbaren Funktionen entsteht, ist es ratsam, die Verantwortlichkeiten für Logik und Status an andere Klassen zu delegieren: State-Holder.

Weitere Informationen finden Sie in der Dokumentation zu State Hoisting in Compose oder auf der Seite State Holders und UI-Status im Architekturleitfaden.

Berechnungen für Erinnerungen neu auslösen, wenn sich Schlüssel ändern

Die remember API wird häufig zusammen mit MutableState verwendet:

var name by remember { mutableStateOf("") }

Hier sorgt die Verwendung der Funktion remember dafür, dass der MutableState-Wert Recompositions übersteht.

Im Allgemeinen wird für remember ein calculation-Lambda-Parameter verwendet. Wenn remember zum ersten Mal ausgeführt wird, wird die Lambda-Funktion calculation aufgerufen und ihr Ergebnis gespeichert. Während der Neuzusammenstellung gibt remember den zuletzt gespeicherten Wert zurück.

Neben dem Zwischenspeichern von Status können Sie mit remember auch beliebige Objekte oder Ergebnisse eines Vorgangs in der Komposition speichern, deren Initialisierung oder Berechnung aufwendig ist. Möglicherweise möchten Sie diese Berechnung nicht bei jeder Neuzusammenstellung wiederholen. Ein Beispiel ist das Erstellen dieses ShaderBrush-Objekts, das ein aufwendiger Vorgang ist:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

In remember wird der Wert gespeichert, bis er die Komposition verlässt. Es gibt jedoch eine Möglichkeit, den im Cache gespeicherten Wert zu entwerten. Die remember API akzeptiert auch einen key- oder keys-Parameter. Wenn sich einer dieser Schlüssel ändert, wird der Cache beim nächsten Mal, wenn die Funktion neu zusammengesetzt wird, durch remember ungültig gemacht und der Lambda-Block für die Berechnung wird noch einmal ausgeführt. Mit diesem Mechanismus können Sie die Lebensdauer eines Objekts in der Komposition steuern. Die Berechnung bleibt gültig, bis sich die Eingaben ändern, und nicht bis der gespeicherte Wert die Komposition verlässt.

Die folgenden Beispiele zeigen, wie dieser Mechanismus funktioniert.

In diesem Snippet wird ein ShaderBrush erstellt und als Hintergrundfarbe eines Box-Composable verwendet. In remember wird die ShaderBrush-Instanz gespeichert, da das Neuerstellen, wie bereits erläutert, aufwendig ist. remember verwendet avatarRes als key1-Parameter, also das ausgewählte Hintergrundbild. Wenn sich avatarRes ändert, wird der Pinsel mit dem neuen Bild neu zusammengesetzt und auf Box angewendet. Das kann passieren, wenn der Nutzer in einer Auswahl ein anderes Bild als Hintergrund auswählt.

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(
        modifier = modifier.background(brush)
    ) {
        /* ... */
    }
}

Im nächsten Snippet wird der Status in eine einfache Status-Holder-Klasse MyAppState verschoben. Sie stellt eine rememberMyAppState-Funktion zum Initialisieren einer Instanz der Klasse mit remember bereit. Das Bereitstellen solcher Funktionen zum Erstellen einer Instanz, die Recompositions übersteht, ist ein gängiges Muster in Compose. Die Funktion rememberMyAppState empfängt windowSizeClass, das als key-Parameter für remember dient. Wenn sich dieser Parameter ändert, muss die App die einfache Status-Holder-Klasse mit dem neuesten Wert neu erstellen. Das kann beispielsweise passieren, wenn der Nutzer das Gerät dreht.

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

Compose verwendet die equals-Implementierung der Klasse, um zu entscheiden, ob sich ein Schlüssel geändert hat, und um den gespeicherten Wert zu invalidieren.

Status mit Schlüsseln über die Neuzusammenstellung hinaus speichern

Die rememberSaveable API ist ein Wrapper für remember, mit dem Daten in einer Bundle gespeichert werden können. Mit dieser API kann der Status nicht nur die Neuzusammensetzung, sondern auch die Neuerstellung von Aktivitäten und das vom System initiierte Beenden von Prozessen überdauern. rememberSaveable empfängt input-Parameter für denselben Zweck, für den remember keys empfängt. Der Cache wird ungültig, wenn sich eine der Eingaben ändert. Beim nächsten Mal, wenn die Funktion neu zusammengesetzt wird, wird der Lambda-Block für die Berechnung von rememberSaveable noch einmal ausgeführt.

Im folgenden Beispiel wird userTypedQuery in rememberSaveable gespeichert, bis sich typedQuery ändert:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

Weitere Informationen

Weitere Informationen zu State und Jetpack Compose finden Sie in den folgenden zusätzlichen Ressourcen.

Produktproben

Codelabs

Videos

Blogs