State und Jetpack Compose

Der Status in einer App ist jeder Wert, der sich im Laufe der Zeit ändern kann. Diese Definition ist sehr breit gefasst und umfasst alles von einer Room-Datenbank bis hin zu einer Variablen in einer Klasse.

Alle Android-Apps zeigen dem Nutzer einen 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.
  • Ripple-Animationen 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 dieser Anleitung geht es um die Verbindung zwischen Status und zusammensetzbaren Funktionen sowie um die APIs, die Jetpack Compose bietet, um einfacher mit dem Status zu arbeiten.

Status und Zusammensetzung

Compose ist deklarativ. Daher kann es nur aktualisiert werden, indem dieselbe zusammensetzbare Funktion mit neuen Argumenten aufgerufen wird. Diese Argumente stellen den UI-Status dar. 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 zusammensetzbaren Funktion muss der neue Status 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 diesen Code ausführen und versuchen, Text einzugeben, passiert nichts. Das liegt daran, dass TextField sich nicht selbst aktualisiert, sondern nur, wenn sich der Parameter value ändert. Das liegt daran, wie Zusammensetzung und Neuzusammensetzung in Compose funktionieren.

Weitere Informationen zur erstmaligen Zusammensetzung und Neuzusammensetzung finden Sie unter In Compose denken.

Status in zusammensetzbaren Funktionen

Zusammensetzbare Funktionen können die remember API verwenden, um ein Objekt im Arbeitsspeicher zu speichern. Ein von remember berechneter Wert wird bei der erstmaligen Zusammensetzung in der Zusammensetzung gespeichert und bei der Neuzusammensetzung zurückgegeben. remember kann verwendet werden, um sowohl veränderliche als auch unveränderliche Objekte zu speichern.

mutableStateOf erstellt ein beobachtbares MutableState<T>, einen beobachtbaren Typ, der in die Compose-Laufzeitumgebung integriert ist.

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

Bei Änderungen an value wird die Neuzusammensetzung aller zusammensetzbaren Funktionen geplant, die value lesen.

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 gleichwertig und dienen als Syntaxzucker für verschiedene Verwendungen des Status. Sie sollten die Deklaration auswählen, die den am einfachsten zu lesenden Code in der zusammensetzbaren Funktion erzeugt, die Sie schreiben.

Für die by-Delegatensyntax 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 zusammensetzbare Funktionen oder sogar als Logik in Anweisungen verwenden, um zu ändern, welche zusammensetzbaren Funktionen 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 Neuzusammensetzungen beibehalten. Bei Konfigurationsänderungen wird der Status jedoch nicht 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 Saver-Objekt übergeben.

Weitere unterstützte Statustypen

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 State<T> konvertieren, damit zusammensetzbare Funktionen bei Statusänderungen 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 wie unten beschrieben hinzu:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() erfasst Werte aus einem Flow auf lebenszyklusbezogene Weise, sodass Ihre App Ressourcen sparen kann. Es stellt den letzten ausgegebenen Wert aus dem Compose State dar. Verwenden Sie diese API als empfohlene Methode zum Erfassen von Flows in Android-Apps.

    Die folgende Abhängigkeit ist in der build.gradle Datei erforderlich (sie muss 2.6.0-beta01 oder höher sein):

Kotlin

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

Groovy

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

    collectAsState ähnelt collectAsStateWithLifecycle, da es auch Werte aus einem Flow erfasst und in Compose State umwandelt.

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

    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 über State dar.

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Zustandsorientiert versus zustandslos

Eine zusammensetzbare Funktion, die remember zum Speichern eines Objekts verwendet, erstellt einen internen Status und macht die zusammensetzbare Funktion zustandsorientiert. HelloContent ist ein Beispiel für eine zustandsorientierte zusammensetzbare Funktion, da sie ihren 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. Zusammensetzbare Funktionen mit internem Status sind jedoch in der Regel weniger wiederverwendbar und schwieriger zu testen.

Eine zustandslose zusammensetzbare Funktion ist eine zusammensetzbare Funktion, die keinen Status enthält. Eine einfache Möglichkeit, Zustandslosigkeit zu erreichen, ist das Status Hoisting.

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

Status-Hoisting

Status-Hoisting in Compose ist ein Muster, bei dem der Status an den 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 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 zusammensetzbare Funktion geeignet sind, sollten Sie sie mit Lambdas definieren.

Status, der auf diese Weise gehoistet wird, hat einige wichtige Eigenschaften:

  • Eine einzige Datenquelle:Wenn wir den Status verschieben, anstatt ihn zu duplizieren, stellen wir sicher, dass es nur eine einzige Datenquelle gibt. So lassen sich Fehler vermeiden.
  • Gekapselt:Nur zustandsorientierte zusammensetzbare Funktionen können ihren Status ändern. Er ist vollständig intern.
  • Freigabefähig:Der gehoistete Status kann für mehrere zusammensetzbare Funktionen freigegeben werden. Wenn Sie name in einer anderen zusammensetzbaren Funktion lesen möchten, können Sie das mit Hoisting tun.
  • Abfangbar:Aufrufer der zustandslosen zusammensetzbaren Funktionen können Ereignisse ignorieren oder ändern, bevor sie den Status ändern.
  • Entkoppelt:Der Status für die zustandslosen zusammensetzbaren Funktionen 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-zusammensetzbaren 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") })
    }
}

Durch das Hoisting des Status aus HelloContent ist es einfacher, die zusammensetzbare Funktion zu verstehen, sie in verschiedenen Situationen wiederzuverwenden und zu testen. HelloContent ist von der Speicherung des Status entkoppelt. Das bedeutet, dass Sie die Implementierung von HelloContent nicht ändern müssen, wenn Sie HelloScreen ändern oder ersetzen.

Das Muster, bei dem der Status nach unten und Ereignisse nach oben fließen, wird als unidirektionaler Datenfluss bezeichnet. In diesem Fall fließt der Status von HelloScreen nach HelloContent und Ereignisse von HelloContent nach HelloScreen. Wenn Sie dem unidirektionalen Datenfluss folgen, können Sie zusammensetzbare Funktionen, die den Status in der UI anzeigen, von den Teilen Ihrer App entkoppeln, die den Status speichern und ändern.

Weitere Informationen finden Sie auf der Seite Wohin sollte der Status gehoistet werden?

Status in Compose wiederherstellen

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

Möglichkeiten zum Speichern des Status

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

Parcelize

Die einfachste Lösung besteht darin, dem Objekt die @Parcelize Annotation 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 zum Konvertieren eines Objekts in eine Reihe von Werten definieren, 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

Um die Schlüssel für die Map nicht definieren zu müssen, 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"))
    }
}

Status-Holder in Compose

Einfaches Status-Hoisting kann in den zusammensetzbaren Funktionen selbst verwaltet werden. Wenn jedoch die Menge des zu verfolgenden Status zunimmt oder die Logik in zusammensetzbaren Funktionen ausgeführt werden muss, ist es eine gute Idee, die Logik- und Statusverantwortlichkeiten an andere Klassen zu delegieren: Status-Holder.

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

Neuberechnung von „remember“ 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 Wert von MutableState Neuzusammensetzungen übersteht.

Im Allgemeinen verwendet remember einen calculation-Lambda-Parameter. Wenn remember zum ersten Mal ausgeführt wird, ruft es das calculation-Lambda auf und speichert das Ergebnis. Bei der Neuzusammensetzung gibt remember den zuletzt gespeicherten Wert zurück.

Neben dem Caching des Status können Sie remember auch verwenden, um beliebige Objekte oder Ergebnisse eines Vorgangs in der Zusammensetzung zu speichern, deren Initialisierung oder Berechnung aufwendig ist. Sie möchten diese Berechnung möglicherweise nicht bei jeder Neuzusammensetzung 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
        )
    )
}

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

Die folgenden Beispiele zeigen, wie dieser Mechanismus funktioniert.

In diesem Snippet wird ein ShaderBrush erstellt und als Hintergrund farbe einer Box zusammensetzbaren Funktion verwendet. remember speichert die ShaderBrush-Instanz , da die Neuerstellung aufwendig ist, wie bereits erklärt. remember verwendet avatarRes als key1-Parameter, das ausgewählte Hintergrundbild. Wenn sich avatarRes ändert, wird der Brush mit dem neuen Bild neu zusammengesetzt und wieder auf die Box angewendet. Das kann passieren, wenn der Nutzer in der 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 gehoistet. Sie stellt eine rememberMyAppState-Funktion bereit, um eine Instanz der Klasse mit remember zu initialisieren. Das Bereitstellen solcher Funktionen zum Erstellen einer Instanz, die Neuzusammensetzungen übersteht, ist ein gängiges Muster in Compose. Die rememberMyAppState Funktion empfängt windowSizeClass, das als der 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 entwertet den gespeicherten Wert.

Status mit Schlüsseln über die Neuzusammensetzung hinaus speichern

Die rememberSaveable-API ist ein Wrapper um remember, mit dem Daten in einem Bundle gespeichert werden können. Mit dieser API kann der Status nicht nur Neuzusammensetzungen, sondern auch der Neuerstellung von Aktivitäten und dem vom System initiierten Beenden von Prozessen überstehen. rememberSaveable empfängt input Parameter für denselben Zweck, für den remember keys empfängt. Der Cache wird entwertet, wenn sich eine der Eingaben ändert. Beim nächsten Mal, wenn die Funktion neu zusammengesetzt wird, führt rememberSaveable den Lambda-Block für die Berechnung 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 zum Status und zu Jetpack Compose finden Sie in den folgenden zusätzlichen Ressourcen.

Beispiele

Codelabs

Videos

Blogs