State und Jetpack Compose

Der Status in einer App ist ein beliebiger Wert, der sich im Laufe der Zeit ändern kann. Dies ist eine sehr weit gefasste 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 zugehörige Kommentare.
  • Wellenanimationen auf Schaltflächen, die abgespielt werden, wenn ein Nutzer darauf klickt.
  • Sticker, die Nutzer über ein Bild zeichnen können.

Mit Jetpack Compose können Sie genau festlegen, 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 für die Arbeit mit dem Status bietet.

Zustand und Zusammensetzung

Compose ist deklarativ. Daher kann es nur aktualisiert werden, wenn dieselbe zusammensetzbare Funktion mit neuen Argumenten aufgerufen wird. Diese Argumente sind Darstellungen des UI-Status. Jedes Mal, wenn ein Status aktualisiert wird, findet eine Neuzusammensetzung statt. Daher werden Elemente wie TextField nicht automatisch aktualisiert, wie es bei imperativen XML-basierten Ansichten der Fall ist. Einer komponierbaren Funktion muss der neue Zustand explizit mitgeteilt werden, damit sie 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 dies ausführen und versuchen, Text einzugeben, passiert nichts. Das liegt daran, dass sich die TextField nicht selbst aktualisiert, sondern nur, wenn sich der Parameter value ändert. Das liegt daran, wie die Komposition und Neukomposition in Compose funktionieren.

Erstmalige Erstellung: Erstellung einer Komposition durch erstmaliges Ausführen von Compose-Funktionen.

Neuzusammensetzung: Erneutes Ausführen von Compose-Funktionen, um die Zusammensetzung zu aktualisieren, wenn sich Daten ändern.

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

Zustand in composables

Komponierbare Funktionen können die remember API verwenden, um ein Objekt im Speicher 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 änderbaren und unveränderlichen Objekten verwendet werden.

mutableStateOf erstellt ein beobachtbares MutableState<T>, das ein beobachtbarer Typ ist, der in die Compose-Laufzeit integriert ist.

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

Alle Änderungen an value führen dazu, dass alle zusammensetzbaren Funktionen, die value lesen, neu zusammengesetzt werden.

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

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

Diese Deklarationen sind äquivalent und werden als syntaktischer Sugar für verschiedene Verwendungen des Status bereitgestellt. Wählen Sie die Option aus, die den am einfachsten zu lesenden Code in der von Ihnen geschriebenen zusammensetzbaren Funktion erzeugt.

Für die Delegatsyntax by sind die folgenden Importe erforderlich:

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

Du kannst 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 kannst du den Status über Neuzusammensetzungen hinweg beibehalten, aber nicht über Konfigurationsänderungen hinweg. 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 Statustypen

Compose erfordert nicht, dass Sie MutableState<T> 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 zusammensetzbare Funktionen bei einer Statusänderung automatisch neu zusammengesetzt werden können.

Compose enthält Funktionen zum Erstellen von State<T> aus gängigen beobachtbaren Typen, die in Android-Apps verwendet werden. Bevor Sie diese Integrationen verwenden, fügen Sie die entsprechenden Artefakte so hinzu:

Kotlin

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

Groovy

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

    collectAsState ähnelt collectAsStateWithLifecycle, da damit auch 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 dieses LiveData und stellt seine Werte mit State dar.

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Zustandsorientiert oder zustandslos

Eine zusammensetzbare Funktion, die remember zum Speichern eines Objekts verwendet, erstellt einen internen Status, wodurch die zusammensetzbare Funktion zustandsorientiert wird. HelloContent ist ein Beispiel für eine zustandsorientierte zusammensetzbare Funktion, da sie ihren name-Zustand intern speichert und ändert. Dies 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. Kompositionen mit internem Status sind jedoch in der Regel weniger wiederverwendbar und schwieriger zu testen.

Eine zustandslose zusammensetzbare Funktion ist eine zusammensetzbare Funktion, die keinen Zustand enthält. Eine gängige Methode, um Zustandslosigkeit zu erreichen, ist das Status-Hoisting.

Wenn Sie wiederverwendbare komponierbare Funktionen entwickeln, möchten Sie oft sowohl eine zustandsorientierte als auch eine zustandslose Version derselben komponierbaren Funktion bereitstellen. Die zustandsorientierte Version ist praktisch für Aufrufer, die sich nicht um den Zustand kümmern, und die zustandslose Version ist für Aufrufer erforderlich, die den Zustand steuern oder hochstufen müssen.

Status-Hoisting

Status-Hoisting in Compose ist ein Muster, bei dem der Status zum Aufrufer einer zusammensetzbaren Funktion verschoben wird, um die zusammensetzbare Funktion zustandslos zu machen. Das allgemeine Muster für das Status-Hoisting in Jetpack Compose besteht darin, die Statusvariable durch zwei Parameter zu ersetzen:

  • value: T: Der aktuelle Wert, der angezeigt werden soll.
  • onValueChange: (T) -> Unit: Ein Ereignis, das die Änderung des Werts anfordert, wobei T der vorgeschlagene neue Wert ist.

Sie sind jedoch nicht auf onValueChange beschränkt. Wenn für das Compose-Element spezifischere Ereignisse geeignet sind, sollten Sie diese mit Lambdas definieren.

Ein auf diese Weise hochgezogener Status hat einige wichtige Eigenschaften:

  • Zentrale Informationsquelle: Indem wir den Status verschieben, anstatt ihn zu duplizieren, sorgen wir dafür, dass es nur eine zentrale Informationsquelle gibt. So lassen sich Fehler vermeiden.
  • Gekapselt:Nur zustandsorientierte Composables können ihren Zustand ändern. Es ist komplett intern.
  • Teilbar:Der hochgezogene Status kann für mehrere Composables freigegeben werden. Wenn Sie name in einer anderen zusammensetzbaren Funktion lesen möchten, können Sie das mit Hoisting tun.
  • Abfangbar: Aufrufer der zustandslosen Composables können Ereignisse ignorieren oder ändern, bevor der Zustand geändert wird.
  • Entkoppelt: Der Status für die zustandslosen Composables kann an einem beliebigen Ort gespeichert werden. Beispielsweise ist es jetzt möglich, name in ein ViewModel zu verschieben.

In diesem Beispiel extrahieren Sie name und onValueChange aus HelloContent und verschieben sie im Baum nach oben zu einer HelloScreen-komponierbaren Funktion, 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 herausnehmen, ist es einfacher, die zusammensetzbare Funktion zu verstehen, sie in verschiedenen Situationen wiederzuverwenden und zu testen. HelloContent ist von der Speicherung des Status entkoppelt. Entkopplung bedeutet, dass Sie die Implementierung von HelloContent nicht ändern müssen, wenn Sie HelloScreen ändern oder ersetzen.

Unidirektionaler Datenfluss

Das Muster, bei dem der Status nach unten und die Ereignisse nach oben gehen, wird als unidirektionaler Datenfluss bezeichnet. In diesem Fall wird der Status von HelloScreen auf HelloContent herabgestuft und Ereignisse werden von HelloContent auf HelloScreen hochgestuft. Wenn Sie dem unidirektionalen Datenfluss folgen, können Sie die Composables, die den Status in der Benutzeroberfläche anzeigen, von den Teilen Ihrer App entkoppeln, die den Status speichern und ändern.

  1. Der Status sollte mindestens auf das niedrigste gemeinsame übergeordnete Element aller Composables hochgezogen werden, die den Status verwenden (lesen).
  2. Der Status sollte mindestens auf die höchste Ebene, auf der er geändert werden kann (Schreiben), hochgezogen werden.
  3. Wenn sich zwei Zustände als Reaktion auf dieselben Ereignisse ändern, sollten sie zusammen hochgezogen werden.

Sie können den Status höher als von diesen Regeln gefordert hochstufen. Wenn Sie den Status jedoch zu niedrig hochstufen, ist es schwierig oder unmöglich, dem unidirektionalen Datenfluss zu folgen.

Weitere Informationen finden Sie auf der Seite Wo der Status hochgezogen werden sollte.

Status in Compose wiederherstellen

Die rememberSaveable API verhält sich ähnlich wie remember, da sie den Status über Neuzusammensetzungen hinweg beibehält und auch über die Neuerstellung von Aktivitäten oder Prozessen hinweg, indem sie den Mechanismus für den gespeicherten Instanzstatus verwendet. Das passiert beispielsweise, wenn der Bildschirm gedreht wird.

Möglichkeiten zum Speichern des Zustands

Alle Datentypen, die dem Bundle hinzugefügt werden, werden automatisch gespeichert. Wenn Sie etwas speichern möchten, das nicht zur Bundle hinzugefügt werden kann, gibt es mehrere Möglichkeiten.

Parcelize

Die einfachste Lösung besteht darin, dem Objekt die Annotation @Parcelize hinzuzufügen. Das Objekt wird parcelable und kann gebündelt werden. Mit diesem Code wird beispielsweise ein parcelable City-Datentyp 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 definieren, um ein Objekt in eine Reihe von Werten zu konvertieren, die das System im 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

Wenn Sie die Schlüssel für die Zuordnung nicht definieren möchten, können Sie auch listSaver und die zugehörigen Indexe als Schlüssel verwenden:

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 Status-Hoisting kann in den zusammensetzbaren Funktionen selbst verwaltet werden. Wenn jedoch die Menge des zu verfolgenden Zustands zunimmt oder die Logik, die in zusammensetzbaren Funktionen ausgeführt werden soll, entsteht, ist es eine gute Praxis, die Logik- und Zustandsverantwortlichkeiten an andere Klassen zu delegieren: State Holder.

In anderen Materialien werden State Holder auch als hochgestufte Zustandsobjekte bezeichnet.

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

Berechnungen für „remember“ neu auslösen, wenn sich Schlüssel ändern

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

var name by remember { mutableStateOf("") }

Wenn Sie hier die Funktion remember verwenden, bleibt der Wert MutableState bei Neuzusammensetzungen erhalten.

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

Neben dem Caching des Status können Sie remember auch verwenden, um ein beliebiges Objekt oder Ergebnis eines Vorgangs in der Composition zu speichern, dessen Initialisierung oder Berechnung aufwendig ist. Sie möchten diese Berechnung wahrscheinlich nicht bei jeder Neuzusammensetzung wiederholen. Ein Beispiel ist das Erstellen dieses ShaderBrush-Objekts, was ein ressourcenintensiver Vorgang ist:

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

remember speichert den Wert, 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 beim nächsten Neuzusammensetzen der Funktion der Cache von remember ungültig gemacht und der Lambda-Block für die Berechnung noch einmal ausgeführt. Mit diesem Mechanismus können Sie die Lebensdauer eines Objekts in der Composition steuern. Die Berechnung bleibt gültig, bis sich die Eingaben ändern, und nicht, bis der gespeicherte Wert die Composition verlässt.

Die folgenden Beispiele zeigen, wie dieser Mechanismus funktioniert.

In diesem Snippet wird ein ShaderBrush erstellt und als Hintergrundfarbe einer Box-Komposition verwendet. remember speichert die ShaderBrush-Instanz, da sie wie bereits erwähnt teuer zu erstellen ist. remember verwendet avatarRes als Parameter key1, also das ausgewählte Hintergrundbild. Wenn sich avatarRes ändert, wird der Pinsel mit dem neuen Bild neu zusammengesetzt und wieder auf Box angewendet. Das kann passieren, wenn der Nutzer 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 State Holder-Klasse MyAppState hochgezogen. Sie stellt eine rememberMyAppState-Funktion bereit, um eine Instanz der Klasse mit remember zu initialisieren. Das Bereitstellen solcher Funktionen zum Erstellen einer Instanz, die Rekompositionen ü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 State 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 den gespeicherten Wert zu entwerten.

Zustand mit Schlüsseln über die Neuzusammensetzung hinaus speichern

Die rememberSaveable API ist ein Wrapper um remember, der Daten in einem Bundle speichern kann. Mit dieser API bleibt der Status nicht nur bei der Neuzusammensetzung, sondern auch bei der Neuerstellung von Aktivitäten und der vom System initiierten Prozessbeendigung erhalten. 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. Wenn die Funktion das nächste Mal neu zusammengesetzt wird, führt rememberSaveable den Lambda-Block für die Berechnung noch einmal aus.

Im folgenden Beispiel speichert rememberSaveable den Wert userTypedQuery, 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 findest du in den folgenden zusätzlichen Ressourcen.

Beispiele

Codelabs

Videos

Blogs