State und Jetpack Compose

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

Alle Android-Apps zeigen den Status für den Nutzer an. Einige Beispiele für 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 auf ein Bild kleben können.

Mit Jetpack Compose können Sie genau 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 Compose-Elementen und um die APIs, die Jetpack Compose bietet, um einfacher mit Status zu arbeiten.

Status und Zusammensetzung

Compose ist deklarativ und kann daher nur durch Aufrufen desselben Composeable mit neuen Argumenten aktualisiert werden. Diese Argumente sind Darstellungen des UI-Status. Jedes Mal, wenn ein Status aktualisiert wird, erfolgt eine Neuzusammensetzung. Daher werden Elemente wie TextField nicht automatisch aktualisiert, wie es in imperativ XML-basierten Ansichten der Fall ist. Ein Composeable 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 das Script ausführen und versuchen, Text einzugeben, passiert nichts. Das liegt daran, dass sich TextField nicht selbst aktualisiert, sondern nur, wenn sich der value-Parameter ändert. Das liegt daran, wie Komposition und Neukomposition in Compose funktionieren.

Weitere Informationen zur ersten Komposition und Neukomposition finden Sie unter Komposition als Denkweise.

Status in komponierbaren Funktionen

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

mutableStateOf erstellt ein Observable MutableState<T>, einen Observable-Typ, der in die Compose-Laufzeit eingebunden ist.

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

Ä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 einem Composeable 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 Zucker für verschiedene Verwendungen von „state“ bereitgestellt. Wählen Sie die Option aus, die im von Ihnen geschriebenen Composeable den am besten lesbaren Code erzeugt.

Für die by-Delegierungssyntax 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 Composeables oder sogar als Logik in Anweisungen verwenden, um zu ändern, welche Composeables angezeigt werden. Wenn Sie beispielsweise nicht möchten, dass die Begrüßung angezeigt wird, 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 Neuzusammensetzungen beibehalten, er wird jedoch nicht bei Konfigurationsänderungen beibehalten. 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 Speicherobjekt übergeben.

Andere unterstützte Statustypen

Für Compose ist es nicht erforderlich, MutableState<T> zum Speichern des Status zu verwenden. Es werden auch andere Observable-Typen unterstützt. Bevor Sie einen anderen Observable-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.

Erstellen Sie Schiffe mit Funktionen, um State<T> aus gängigen Observable-Typen zu erstellen, die in Android-Apps verwendet werden. Bevor Sie diese Integrationen verwenden, fügen Sie die entsprechenden Artefakte hinzu, wie unten beschrieben:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() ruft Werte aus einem Flow nutzungsabhängig ab, sodass Ihre App App-Ressourcen einsparen kann. Er entspricht dem zuletzt gesendeten Wert von „Compose“ State. Diese API ist die empfohlene Methode zum Erfassen von Abläufen in Android-Apps.

    In der Datei build.gradle ist die folgende Abhängigkeit erforderlich (Version 2.6.0-beta01 oder höher):

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 einer Flow erfasst und in „Komponieren“ 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, diese LiveData zu beobachten und stellt ihre Werte über State dar.

    In der Datei build.gradle ist die folgende Abhängigkeit erforderlich:

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Zustandsorientiert und zustandslos

Ein Composeable, das ein Objekt mit remember speichert, erstellt einen internen Status und ist somit zustandsabhängig. HelloContent ist ein Beispiel für ein zustandsorientiertes Kompositum, da es seinen name-Status 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 ihn selbst verwalten zu müssen. Allerdings sind Composeables mit internem Status in der Regel weniger wiederverwendbar und schwieriger zu testen.

Ein zustandsloser Composeable ist ein Composeable, das keinen Status hat. Eine einfache Möglichkeit, einen zustandslosen Dienst zu implementieren, ist das Staus-Hoisting.

Wenn Sie wiederverwendbare Composeables entwickeln, möchten Sie oft sowohl eine zustandsorientierte als auch eine zustandslose Version desselben Composeables freigeben. Die zustandsorientierte Version ist für Aufrufer praktisch, die sich nicht um den Status kümmern müssen. Die zustandslose Version ist für Aufrufer erforderlich, die den Status steuern oder anheben müssen.

Status-Hoisting

Das Zustands-Hoisting in Compose ist ein Muster, bei dem der Zustand an den Aufrufer eines Compose-Objekts verschoben wird, um es zustandslos zu machen. Beim allgemeinen Muster für das Heben des Status in Jetpack Compose wird die Statusvariable durch zwei Parameter ersetzt:

  • value: T:Der aktuelle Wert, der angezeigt werden soll
  • onValueChange: (T) -> Unit:Ereignis, bei dem die Änderung des Werts angefordert wird. T ist der vorgeschlagene neue Wert.

Sie sind jedoch nicht auf onValueChange beschränkt. Wenn für die Composeable-Funktion spezifischere Ereignisse geeignet sind, sollten Sie sie mit Lambdas definieren.

So ausgelagerter Status hat einige wichtige Eigenschaften:

  • Single Source of Truth: Indem wir den Status verschieben, anstatt ihn zu duplizieren, sorgen wir dafür, dass es nur eine einzige „Source of Truth“ gibt. So lassen sich Fehler vermeiden.
  • Eingekapselt:Nur zustandsorientierte Composeables können ihren Status ändern. Sie ist vollständig intern.
  • Teilbar:Der gehostete Status kann für mehrere Composeables freigegeben werden. Wenn Sie name in einem anderen Composeable lesen möchten, ist das mithilfe von Hoisting möglich.
  • Abfangbar:Aufrufer der zustandslosen Composeables können Ereignisse ignorieren oder ändern, bevor der Status geändert wird.
  • Entkoppelt:Der Status der zustandslosen Composeables kann überall gespeichert werden. So ist es beispielsweise jetzt möglich, name in eine ViewModel zu verschieben.

Im Beispiel extrahieren Sie name und onValueChange aus HelloContent und verschieben sie im Baum nach oben zu einem HelloScreen-Komposit, das 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 der Status aus HelloContent ausgelagert wird, lässt sich die Komponente leichter analysieren, in verschiedenen Situationen wiederverwenden und testen. HelloContent ist von der Speicherung des Status entkoppelt. Wenn Sie HelloScreen ändern oder ersetzen, müssen Sie die Implementierung von HelloContent nicht ändern.

Das Muster, bei dem der Status sinkt und die Ereignisse steigen, wird als einseitiger Datenfluss bezeichnet. In diesem Fall sinkt der Status von HelloScreen auf HelloContent und die Anzahl der Ereignisse steigt von HelloContent auf HelloScreen. Wenn Sie einem unidirektionalen Datenfluss folgen, können Sie Komponenten, die den Status in der Benutzeroberfläche anzeigen, von den Teilen Ihrer App entkoppeln, die den Status speichern und ändern.

Weitere Informationen finden Sie auf der Seite Wo Status hochgehängt werden sollte.

Status in der Zeichenansicht wiederherstellen

Die rememberSaveable API verhält sich ähnlich wie remember, da der Status bei Neuzusammensetzungen und auch bei der Neuerstellung von Aktivitäten oder Prozessen mithilfe des Mechanismus für den gespeicherten Instanzstatus beibehalten wird. Das passiert beispielsweise, wenn das Display gedreht wird.

Möglichkeiten zum Speichern des Zustands

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

Parcelize

Die einfachste Lösung besteht darin, dem Objekt die Annotation @Parcelize hinzuzufügen. Das Objekt kann dann in Pakete aufgeteilt und in Sets angeboten werden. Mit diesem Code wird beispielsweise ein teilbarer 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 für die Umwandlung eines Objekts in eine Reihe von Werten definieren, die das System in der 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 verwenden und die 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

Das einfache Zustands-Hoisting kann in den zusammensetzbaren Funktionen selbst verwaltet werden. Wenn jedoch die Menge an Status, die Sie im Blick behalten müssen, zunimmt oder die Logik in kompositionsfähigen Funktionen ausgeführt werden muss, sollten Sie die Verantwortung für Logik und Status an andere Klassen delegieren: Statushalter.

Weitere Informationen finden Sie in der Dokumentation zum Zustandsaufstieg in Compose oder allgemeiner im Architekturleitfaden auf der Seite Statushalter und UI-Status.

Berechnungen für „Denken Sie daran“ bei Schlüsseländerungen noch einmal auslösen

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

var name by remember { mutableStateOf("") }

Hier bleibt der MutableState-Wert durch die Verwendung der Funktion remember bei Umordnungen erhalten.

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

Neben dem Caching-Status können Sie mit remember auch Objekte oder Ergebnisse eines Vorgangs in der Komposition speichern, deren Initialisierung oder Berechnung aufwendig ist. Sie müssen diese Berechnung nicht bei jeder Neuzusammensetzung wiederholen. Ein Beispiel ist das Erstellen dieses ShaderBrush-Objekts, was ein teurer 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 den Parameter key oder keys. Wenn sich einer dieser Schlüssel ändert, remember erhebt den Cache auf und führt den Lambda-Block noch einmal aus, wenn die Funktion das nächste Mal neu erstellt wird. 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-Composeables verwendet. remember speichert die Instanz ShaderBrush, da das Erstellen sehr aufwendig ist, wie bereits erläutert. Für remember wird avatarRes als key1-Parameter verwendet, also das ausgewählte Hintergrundbild. Wenn sich avatarRes ändert, wird der Pinsel mit dem neuen Bild neu zusammengesetzt und auf die 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 Statushalterklasse MyAppState verschoben. Sie stellt eine rememberMyAppState-Funktion bereit, um eine Instanz der Klasse mit remember zu initialisieren. Das Bereitstellen solcher Funktionen zum Erstellen einer Instanz, die bei Neuzusammensetzungen erhalten bleibt, 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 Statushalterklasse mit dem aktuellen Wert neu erstellen. Das kann z. B. 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 ungültig zu machen.

Status mit Schlüsseln speichern, die über die Neuzusammensetzung hinausgehen

Die rememberSaveable API ist ein Wrapper für remember, mit dem Daten in einer Bundle gespeichert werden können. Mit dieser API bleibt der Status nicht nur nach der Neuzusammensetzung, sondern auch nach der Wiederherstellung von Aktivitäten und dem systeminitiierten Beenden des Prozesses erhalten. rememberSaveable empfängt input-Parameter zu demselben Zweck wie remember keys. Der Cache wird ungültig, wenn sich eine der Eingaben ändert. Wenn die Funktion das nächste Mal neu erstellt wird, führt rememberSaveable den Berechnungs-Lambda-Block noch einmal aus.

Im folgenden Beispiel speichert rememberSaveable 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 Status und Jetpack Compose finden Sie in den folgenden zusätzlichen Ressourcen.

Produktproben

Codelabs

Videos

Blogs