State und Jetpack Compose

Der Status in einer App ist jeder Wert, der sich im Laufe der Zeit ändern kann. Dies ist eine sehr weit gefasste Definition und umfasst alles von einer Raumdatenbank bis zu einer Variablen in einer Klasse.

Der Status aller Android-Apps wird dem Nutzer angezeigt. Einige Beispiele für Status in Android-Apps:

  • Eine Snackbar, die anzeigt, wenn keine Netzwerkverbindung hergestellt werden kann.
  • Ein Blogpost und zugehörige Kommentare.
  • Wellenförmige Animationen auf Schaltflächen, die abgespielt werden, wenn ein Nutzer darauf klickt.
  • Sticker, die Nutzende auf einem Bild zeichnen können.

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 auf die APIs, die Jetpack Compose für die einfachere Arbeit mit Statusfunktionen bietet.

Status und Zusammensetzung

Die Funktion "Compose" ist deklarativ. Daher kann sie nur durch Aufrufen derselben zusammensetzbaren Funktion mit neuen Argumenten aktualisiert werden. Diese Argumente sind Darstellungen des UI-Status. Jedes Mal, wenn ein Status aktualisiert wird, findet eine Neuzusammensetzung statt. Daher werden Dinge wie TextField nicht automatisch aktualisiert, wie dies in imperativen XML-basierten Ansichten der Fall ist. Einer zusammensetzbaren Funktion muss explizit der neue Status 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 Befehl ausführen und versuchen, Text einzugeben, werden Sie feststellen, dass nichts passiert. Das liegt daran, dass TextField nicht automatisch aktualisiert wird, sondern wenn sich der value-Parameter ändert. Dies liegt an der Funktionsweise von Komposition und Neuzusammensetzung in Composer.

Weitere Informationen zur anfänglichen Komposition und Neuzusammensetzung finden Sie unter Thinking in Compose.

Status in zusammensetzbaren Funktionen

Zusammensetzbare Funktionen können mit der remember API ein Objekt im Arbeitsspeicher speichern. Ein von remember berechneter Wert wird während der anfänglichen Zusammensetzung in der Zusammensetzung gespeichert. Der gespeicherte Wert wird bei der Neuzusammensetzung zurückgegeben. Mit remember können sowohl änderbare als auch unveränderliche Objekte gespeichert werden.

Mit mutableStateOf wird ein beobachtbarer MutableState<T> erstellt. Dies ist ein beobachtbarer Typ, der in die Compose-Laufzeit eingebunden 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 äquivalent und werden als Syntax Zucker für verschiedene Verwendungen von Zustand bereitgestellt. Sie sollten diejenige auswählen, mit der der Code in der von Ihnen geschriebenen zusammensetzbaren Funktion am einfachsten zu lesen ist.

Die Delegatsyntax by erfordert die folgenden Importe:

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, der Status wird jedoch bei Konfigurationsänderungen 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.

Andere unterstützte Statustypen

Zum Speichern des Status muss MutableState<T> nicht verwendet werden. Es unterstützt andere beobachtbare Typen. Bevor Sie einen anderen beobachtbaren Typ in Composer lesen, müssen Sie ihn in eine State<T> konvertieren, damit zusammensetzbare Funktionen automatisch neu zusammengesetzt werden können, wenn sich der Status ändert.

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 hinzu, wie unten beschrieben:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() erfasst Werte aus einem Flow unter Beachtung des Lebenszyklus, sodass die Anwendung Anwendungsressourcen schont. Es stellt den letzten ausgegebenen Wert aus der Erstellung State dar. Verwenden Sie diese API als empfohlene Methode zum Erfassen von Abläufen in Android-Apps.

    Die folgende Abhängigkeit 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.6.2")
}

Cool

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

    collectAsState ähnelt collectAsStateWithLifecycle, da damit auch Werte aus einer Flow erfasst und in das Schreiben-State-Element umgewandelt werden.

    Verwende collectAsState für plattformunabhängigen Code anstelle von collectAsStateWithLifecycle, da dieser 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 Datei build.gradle erforderlich:

Kotlin

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

Cool

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

Kotlin

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

Cool

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

Kotlin

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

Cool

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

Zustandsorientiert oder 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 den Status name intern enthält und ändert. Dies kann in Situationen nützlich sein, in denen ein Aufrufer den Status nicht steuern muss, sondern ihn verwenden kann, ohne den Status selbst verwalten zu müssen. Zusammensetzbare Funktionen mit internem Status sind jedoch in der Regel weniger wiederverwendbar und schwerer zu testen.

Eine zustandslose zusammensetzbare Funktion ist eine zusammensetzbare Funktion, die keinen Zustand enthält. Eine einfache Möglichkeit, zustandslos zu arbeiten, ist die Verwendung von Zustandsheben.

Bei der Entwicklung wiederverwendbarer zusammensetzbarer Funktionen empfiehlt es sich häufig, sowohl eine zustandsorientierte als auch eine zustandslose Version derselben zusammensetzbaren Funktion zur Verfügung zu stellen. Die zustandsorientierte Version ist für Aufrufer geeignet, für die der Status nicht relevant ist, und die zustandslose Version für Aufrufer, die den Status steuern oder hochfahren müssen.

Staatliche Winden

Das Hochheben von Status in Compose ist ein Muster, das sich in den Aufrufer einer zusammensetzbaren Funktion bewegt, um eine zusammensetzbare zustandslose zu machen. Das allgemeine Muster für Zustandswinden in Jetpack Compose besteht darin, die Zustandsvariable durch zwei Parameter zu ersetzen:

  • value: T: der aktuell 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 für die zusammensetzbare Funktion spezifischere Ereignisse geeignet sind, sollten Sie sie mithilfe von Lambdas definieren.

Ein Zustand, der auf diese Weise gezogen wird, hat einige wichtige Eigenschaften:

  • Single Source of Truth:Durch das Verschieben des Zustands anstelle des Duplizierens wird sichergestellt, dass es nur eine einzige Informationsquelle gibt. So lassen sich Fehler vermeiden.
  • Encapsulated (Verkapselt): Der Status kann nur von zustandsorientierten zusammensetzbaren Funktionen geändert werden. Sie ist komplett intern.
  • Gemeinsam nutzbar: Der Hub-Zustand kann mit mehreren zusammensetzbaren Funktionen geteilt werden. Wenn Sie name in einer anderen zusammensetzbaren Funktion lesen möchten, können Sie dies mit Winden erreichen.
  • Interceptable:Aufrufer der zustandslosen zusammensetzbaren Funktionen können Ereignisse vor dem Ändern des Status ignorieren oder ändern.
  • Entkoppelt: Der Status für die zustandslosen zusammensetzbaren Funktionen kann überall gespeichert werden. Beispielsweise ist es jetzt möglich, name in einen ViewModel zu verschieben.

Im Beispielfall extrahieren Sie name und onValueChange aus HelloContent und verschieben sie in der Baumstruktur nach oben in eine zusammensetzbare HelloScreen-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 Zustand aus HelloContent verschieben, ist es einfacher, die zusammensetzbare Funktion zu analysieren, sie in verschiedenen Situationen zu verwenden und zu testen. HelloContent wird von der Speicherung seines Status entkoppelt. Wenn Sie HelloScreen ändern oder ersetzen, müssen Sie die Implementierung von HelloContent nicht ändern.

Das Muster, bei dem der Zustand abfällt und die Ereignisse ansteigen, wird als unidirektionaler Datenfluss bezeichnet. In diesem Fall sinkt der Status von HelloScreen auf HelloContent und die Ereignisse steigen von HelloContent auf HelloScreen an. Indem Sie dem unidirektionalen Datenfluss folgen, können Sie zusammensetzbare Funktionen, die den Status in der UI anzeigen, von den Teilen Ihrer App entkoppeln, die ihren Status speichern und ändern.

Weitere Informationen finden Sie auf der Seite Windenstatus.

Status in Compose wird wiederhergestellt

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 mit dem 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 Bundle nicht hinzugefügt werden kann, haben Sie mehrere Möglichkeiten.

Paket

Die einfachste Lösung besteht darin, dem Objekt die Annotation @Parcelize hinzuzufügen. Das Objekt wird geparst und kann gebündelt werden. Mit diesem Code wird beispielsweise ein parzellenbarer 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 Ihre eigene Regel zum Konvertieren eines Objekts in eine Gruppe 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"))
    }
}

Listenspeicher

Damit Sie die Schlüssel für die Zuordnung nicht definieren 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"))
    }
}

Staatsinhaber in Compose

Einfache Winden können in den zusammensetzbaren Funktionen selbst verwaltet werden. Wenn jedoch die Menge an Zustand, mit der Erhöhungen verfolgt werden sollen, oder die Logik, die in zusammensetzbaren Funktionen ausgeführt werden soll, eintritt, empfiehlt es sich, die Logik- und Statuszuständigkeiten an andere Klassen zu delegieren: State Owners.

Weitere Informationen finden Sie in der Dokumentation zu State Hoisting in Compose oder allgemein auf der Seite State holder and UI State im Architekturleitfaden.

„Berechnungen merken“ noch einmal auslösen, wenn sich Tasten ändern

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

var name by remember { mutableStateOf("") }

Hier sorgt die remember-Funktion dafür, dass der MutableState-Wert Neuzusammensetzungen überdauert.

Im Allgemeinen verwendet remember einen Lambda-Parameter calculation. Bei der ersten Ausführung von remember wird das Lambda calculation aufgerufen und das Ergebnis gespeichert. Bei der Neuzusammensetzung gibt remember den zuletzt gespeicherten Wert zurück.

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

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 verwendet auch einen key- oder keys-Parameter. Wenn sich einer dieser Schlüssel ändert, wird bei der nächsten Neuzusammensetzung der Funktion remember den Cache ungültig machen und den Lambda-Block für die Berechnung noch einmal ausführen. 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 so lange, bis der gemerkte Wert die Zusammensetzung verlässt.

Die folgenden Beispiele zeigen, wie dieser Mechanismus funktioniert.

In diesem Snippet wird ein ShaderBrush erstellt und als Hintergrundpaint für eine zusammensetzbare Box-Funktion verwendet. remember speichert die Instanz ShaderBrush, da die Neuerstellung, wie oben erläutert, kostspielig ist. remember verwendet avatarRes als key1-Parameter. Dies ist das ausgewählte Hintergrundbild. Wenn sich avatarRes ändert, wird der Pinsel mit dem neuen Bild neu zusammengesetzt und auf Box angewendet. Dies kann vorkommen, wenn der Nutzer ein anderes Bild als Hintergrund in einer Auswahl 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 Zustand in eine einfache State Holder-Klasse MyAppState hochgezogen. Sie stellt eine rememberMyAppState-Funktion zur Verfügung, um eine Instanz der Klasse mit remember zu initialisieren. Ein gängiges Muster in Compose ist es, solche Funktionen zum Erstellen einer Instanz verfügbar zu machen, die Neuzusammensetzungen übersteht. Die Funktion rememberMyAppState empfängt windowSizeClass, das als key-Parameter für remember dient. Wenn sich dieser Parameter ändert, muss die Anwendung die einfache State Holder-Klasse mit dem neuesten Wert neu erstellen. Dies kann beispielsweise der Fall sein, 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 ein Schlüssel geändert wurde, und den gespeicherten Wert entwertet.

Status mit Schlüsseln über die Neuzusammensetzung hinaus speichern

Die rememberSaveable API ist ein Wrapper um remember, der Daten in einem Bundle speichern kann. Diese API ermöglicht es dem Status nicht nur, die Neuzusammensetzung, sondern auch die Wiederherstellung von Aktivitäten und den vom System initiierten Prozess zu überleben. rememberSaveable empfängt input-Parameter für denselben Zweck wie remember keys. Der Cache wird ungültig, wenn sich eine der Eingaben ändert. Bei der nächsten Neuzusammensetzung der Funktion 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 zu Status und Jetpack Compose finden Sie in den folgenden zusätzlichen Ressourcen.

Produktproben

Codelabs

Videos

Blogs