Android Coden
Android 7 min lesen

Jetpack Compose: State mit remember speichern

Lerne, wie du mit remember den Zustand in Jetpack Compose über Recompositions hinweg speicherst und UI-Status effizient verwaltest.

Jetpack Compose basiert auf einem reaktiven und deklarativen Modell, bei dem Benutzeroberflächen durch Funktionen beschrieben werden, die sich bei Datenänderungen stetig aktualisieren. Dieser kontinuierliche Prozess des Neuzeichnens erfordert eine klare Strategie zur Verwaltung von Zustandsdaten. Ohne einen gezielten Mechanismus würden alle Variablen innerhalb einer Funktion bei jedem Aufruf auf ihren Initialwert zurückgesetzt werden. Genau hier setzt ein zentrales Werkzeug des Frameworks an, um einen temporären Datenspeicher direkt im Komponentenbaum zu verankern. Diese Speicherfunktion ist essenziell, um flüchtige Informationen effizient zu verwalten und sicherzustellen, dass die grafische Oberfläche performant und konsistent bleibt.

Was ist das?

Die Funktion remember ist ein fundamentaler Baustein in Jetpack Compose, der einer Composable-Funktion ein eigenes Gedächtnis verleiht. In einem deklarativen UI-Framework wie Compose werden Benutzeroberflächen nicht durch direkte Modifikation von Views aktualisiert, sondern durch den erneuten Aufruf der entsprechenden Funktionen mit neuen Parametern. Dieser Prozess wird als Recomposition bezeichnet. Wenn eine Funktion neu aufgerufen wird, verhält sie sich wie jede reguläre Kotlin-Funktion: Alle lokalen Variablen werden neu initialisiert und berechnet. Wenn du also einen Zustand, wie etwa den Text einer Eingabemaske oder den Status einer aufklappbaren Liste, direkt in einer lokalen Variablen ablegen würdest, ginge dieser Zustand bei der nächsten Recomposition unweigerlich verloren.

Um dieses Problem zu lösen, führt Compose das Konzept des Composition Memory ein. Wenn du einen Wert in remember einwickelst, weist du das Framework an, diesen Wert im Hintergrundbaum der UI-Komponentenstruktur zu speichern. Bei der initialen Ausführung der Funktion, der sogenannten Initial Composition, wird der übergebene Lambda-Ausdruck ausgewertet und das Ergebnis im Composition Memory abgelegt. Bei allen nachfolgenden Recompositions wird der im Speicher hinterlegte Wert abgerufen und zurückgegeben, anstatt die Initialisierung erneut durchzuführen. Dies ermöglicht es dir, einen Local State zu definieren – also einen Zustand, der exklusiv von einer spezifischen UI-Komponente verwaltet wird und nicht in ein übergeordnetes ViewModel ausgelagert werden muss.

Local State umfasst typischerweise flüchtige Daten, die nur für die visuelle Darstellung relevant sind. Dazu gehören Animationszustände, Scroll-Positionen innerhalb einer Liste, der geöffnete oder geschlossene Status eines Dropdown-Menüs oder temporäre Benutzereingaben, bevor diese validiert und an die Geschäftslogik übermittelt werden. Durch die gezielte Nutzung dieser Speicherfunktion trennst du reine UI-Zustände sauber von der eigentlichen Anwendungslogik, was die Architektur deiner App robuster und testbarer macht.

Ein weiterer wichtiger Aspekt ist die Performanz. Neben der reinen Zustandsspeicherung dient der Mechanismus auch dazu, rechenintensive Operationen zu optimieren. Wenn du beispielsweise eine komplexe Liste filterst oder ein aufwendiges Kryptografie-Objekt instanziierst, möchtest du nicht, dass diese Operation bei jedem noch so kleinen UI-Update wiederholt wird. Indem du das Ergebnis einer solchen Berechnung speicherst, fungiert das System als Caching-Schicht, die die CPU-Auslastung minimiert und Framedrops verhindert.

Wie funktioniert es?

Der Mechanismus hinter der Zustandsspeicherung in Compose beruht auf dem Konzept der Positional Memoization. Im Gegensatz zu traditionellen Caches, die Werte anhand expliziter Schlüsselwörter speichern, merkt sich Compose Werte basierend auf der exakten Position des Aufrufs innerhalb der Struktur der Composable-Funktionen. Der Compiler transformiert deinen Code während des Build-Prozesses und fügt unsichtbare Identifikatoren hinzu, die den Speicherplatz im Composition-Baum lokalisieren. Solange die Composable-Funktion nicht aus dem Baum entfernt wird, bleibt der assoziierte Speicherbereich erhalten.

Der Lebenszyklus eines gespeicherten Wertes ist eng an den Lebenszyklus der aufrufenden Komponente gebunden. Wenn die Komponente erstmals gerendert wird, betritt sie die Composition, und der Speicher wird alloziert. Sobald die Komponente durch bedingte Logik wie eine if-Anweisung nicht mehr aufgerufen wird, verlässt sie die Composition. In diesem Moment räumt das Framework den zugehörigen Speicher automatisch auf, und der Zustand wird gelöscht. Wenn die Komponente später erneut gerendert wird, beginnt der Prozess von vorn mit dem Initialwert. Dies garantiert eine effiziente Speichernutzung, da ungenutzte Zustände nicht als Datenmüll im System verbleiben.

In den meisten Fällen wird die Speicherung mit dem Datentyp MutableState kombiniert. Während remember lediglich dafür sorgt, dass eine Objektreferenz über Recompositions hinweg erhalten bleibt, sorgt MutableState dafür, dass Änderungen an diesem Objekt vom Framework registriert werden. Wenn du einen regulären String speicherst und diesen nachträglich änderst, bekommt Compose davon nichts mit. Das UI würde sich nicht aktualisieren. Kapselst du den String jedoch in einem MutableState, abonniert Compose diese Datenstruktur. Jede Änderung am enthaltenen Wert löst automatisch eine Invalidation aus, wodurch eine Recomposition exakt der Komponenten angestoßen wird, die diesen speziellen Zustand lesen. Diese Kombination aus Werterhaltung und reaktiver Benachrichtigung bildet das Rückgrat der interaktiven UI-Entwicklung in Android.

Ein entscheidendes Feature der Speichermechanik ist die Möglichkeit, Parameter zur Invalidierung anzugeben, die sogenannten Keys. Manchmal möchtest du, dass ein gespeicherter Wert gezielt neu berechnet wird, wenn sich eine bestimmte externe Bedingung ändert. Du kannst einen oder mehrere Schlüssel als Argumente übergeben. Bei jeder Recomposition vergleicht das Framework die aktuellen Schlüssel mit den Schlüsseln des vorherigen Durchlaufs. Sind die Werte identisch, wird der gespeicherte Wert aus dem Cache zurückgegeben. Unterscheidet sich auch nur ein einziger Schlüssel, wird der Lambda-Ausdruck erneut ausgeführt, der neue Wert im Composition Memory überschrieben und fortan verwendet. Dies ist besonders nützlich, wenn du Berechnungen durchführst, die von übergebenen Argumenten der Funktion abhängen.

Es ist wichtig zu verstehen, dass dieser lokale Speicher nicht persistent ist. Er überlebt Recompositions, aber er überlebt weder das Entfernen der Komponente aus dem Baum noch Konfigurationsänderungen der Activity. Wenn der Benutzer sein Smartphone dreht, in den Dark-Mode wechselt oder die Sprache umstellt, wird die zugrunde liegende Android-Activity zerstört und neu erstellt. Dies führt unweigerlich zu einem vollständigen Verlust des Composition Memory. Für Zustände, die solche Ereignisse überstehen müssen, greift man auf eine alternative Variante zurück, die Daten in einem Bundle serialisiert und bei der Wiederherstellung der Activity neu lädt.

In der Praxis

Um das Konzept in der täglichen Android-Entwicklung greifbar zu machen, betrachten wir die Implementierung einer einfachen Schaltfläche, die zählt, wie oft sie geklickt wurde. Dies ist ein klassischer Anwendungsfall für Local State, da die Anzahl der Klicks vorerst nur für die Darstellung auf dieser speziellen Schaltfläche relevant ist und nicht sofort an eine Datenbank oder ein Backend gesendet werden muss.

Wenn du den Zähler ohne den Speichermechanismus implementierst, würdest du lediglich eine reguläre Variable deklarieren, die bei jeder Recomposition wieder auf null fällt. Die korrekte Implementierung erfordert die Zuweisung des Zustands innerhalb des Composition Memory zusammen mit einem reaktiven Typ:

@Composable
fun ClickCounter() {
    // Der Zustand wird über Recompositions hinweg gespeichert
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Klicks: $count")
    }
}

Durch die Kombination von Speicherung (remember) und Reaktivität (mutableStateOf) garantiert das System, dass die Variable bei der ersten Darstellung mit null initialisiert wird. Wenn der Klick-Listener den Wert um eins erhöht, benachrichtigt der reaktive Typ das Framework. Die Funktion wird neu aufgebaut, greift auf den reservierten Speicherplatz zu, entnimmt die aktualisierte Zahl und zeichnet den neuen Text.

Eine der häufigsten Stolperfallen in der Praxis ist die Verwechslung von flüchtigem Local State mit persistentem State. Ein typisches Szenario ist die Eingabe von Text in ein Registrierungsformular. Wenn du den Text einer TextField-Komponente lediglich mit dem Standard-Speichermechanismus sicherst, verschwindet der Text komplett, sobald der Nutzer sein Telefon quer hält und die Activity neu startet. In solchen Fällen ist es zwingend erforderlich, auf rememberSaveable auszuweichen, welches die Daten über Konfigurationsänderungen hinweg in einem Bundle rettet. Die Regel lautet: Nutze remember für reine UI- oder Animationszustände und rememberSaveable für alle Daten, die ein Nutzer manuell eingegeben hat.

Darüber hinaus solltest du darauf achten, dass du die Keys richtig einsetzt. Wenn du ein teures Objekt instanziierst, das von einem Parameter abhängt, musst du diesen Parameter als Key übergeben (z.B. remember(userId) { fetchUserAvatar(userId) }). Vergisst du den Key, wird das Objekt bei einer Änderung der ID nicht aktualisiert, da der alte Wert hartnäckig im Cache verbleibt.

Fazit

Die Fähigkeit, Zustände effizient zu speichern und abzurufen, ist das Herzstück einer jeden responsiven Jetpack Compose-Anwendung. Indem du verstehst, wie Werte im Komponentenbaum verankert und über Recompositions hinweg bewahrt werden, kannst du Benutzeroberflächen entwickeln, die sowohl performant als auch stabil sind. Es ist entscheidend, stets kritisch zu hinterfragen, welche Daten wirklich in die UI-Schicht gehören und welche besser im ViewModel aufgehoben sind. Prüfe bei deinem nächsten Projekt gezielt, wie sich deine Formulare und interaktiven Elemente verhalten, wenn du den Bildschirm drehst oder die App in den Hintergrund schiebst. Nutze den Layout Inspector von Android Studio, um Recompositions zu beobachten, und verifiziere durch gezielte UI-Tests, dass deine Komponenten Zustandsänderungen exakt so verarbeiten, wie du es beabsichtigt hast. So stellst du sicher, dass dein Code nicht nur funktional, sondern auch robust für den produktiven Einsatz ist.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.