Derived State in Jetpack Compose: Effizienz durch abgeleitete Zustände
Optimiere deine Compose-UIs mit derivedStateOf. Lerne, wie du teure Berechnungen vermeidest und Recompositions intelligent steuerst.
In Jetpack Compose ist der Zustand deiner App der Motor, der die Benutzeroberfläche antreibt. Wenn sich Daten ändern, wird die UI automatisch aktualisiert, um diese neuen Informationen widerzuspiegeln. Doch nicht jede Zustandsänderung sollte zwangsläufig zu einem vollständigen Neuzeichnen der betroffenen Komponenten führen. Besonders bei komplexen Ansichten oder Werten, die sich in hoher Frequenz ändern, kann eine unüberlegte Zustandsverwaltung die Performance deiner App drastisch reduzieren. An dieser Stelle kommt derivedStateOf ins Spiel. Es ist ein mächtiges Werkzeug, das dir hilft, die Effizienz deiner Applikation zu wahren, indem es Recompositions präzise steuert und unnötige Berechnungen blockiert. Die reaktive Natur von Jetpack Compose erfordert ein Umdenken im Vergleich zum klassischen, imperativen View-System von Android. Du sagst der UI nicht mehr explizit, dass sie sich ändern soll, sondern du deklarierst, wie die UI in Abhängigkeit vom aktuellen Zustand aussehen soll.
Was ist das?
Unter “Derived State”, also einem abgeleiteten Zustand, versteht man in Jetpack Compose einen Wert, der auf der Basis von einem oder mehreren anderen Zustandsobjekten berechnet wird. Die fundamentale Idee hinter diesem Konzept ist die Vermeidung von überflüssigen Aktualisierungen der Benutzeroberfläche. Stell dir vor, du hast einen Zustand, der sich kontinuierlich verändert, wie beispielsweise die genaue Scroll-Position einer langen Liste in Pixeln oder der exakte Index des ersten sichtbaren Elements. Deine UI muss aber nicht bei jedem einzelnen Pixel, der gescrollt wird, neu gezeichnet werden. Vielleicht möchtest du nur einen Button einblenden, sobald der Nutzer weiter als bis zum zehnten Element gescrollt hat.
Ein abgeleiteter Zustand fungiert als intelligenter Filter zwischen den sich schnell ändernden Rohdaten und deiner UI-Komponente. Anstatt die Komponente bei jeder winzigen Änderung des Ursprungszustands zu benachrichtigen, wertet der abgeleitete Zustand die Daten aus und benachrichtigt die UI erst dann, wenn sich das berechnete Ergebnis tatsächlich verändert hat. Dieser Ansatz, auch als “Computed State” bekannt, ist essenziell für die Effizienz in modernen Android-Anwendungen.
Wenn du Jetpack Compose verwendest, verlässt du dich darauf, dass das Framework intelligent genug ist, nur die Teile der UI neu zu zeichnen, die sich wirklich geändert haben. Das Framework kann jedoch nicht immer von selbst wissen, ob eine Berechnung teuer ist oder ob eine Zustandsänderung für eine bestimmte Komponente relevant ist. Durch die explizite Definition eines abgeleiteten Zustands gibst du Compose einen direkten Hinweis darauf, wie es mit den Daten umgehen soll. Du entkoppelst die Frequenz der Datenänderung von der Frequenz der UI-Aktualisierung. Ohne dieses Konzept würdest du riskieren, dass rechenintensive Operationen bei jedem einzelnen Frame neu ausgeführt werden, was schnell zu Framedrops und einer stotternden Nutzererfahrung führt.
Wie funktioniert es?
Die Funktion derivedStateOf nimmt einen Lambda-Ausdruck entgegen. Innerhalb dieses Blocks kannst du andere Zustandsobjekte (State<T>) lesen und daraus einen neuen Wert berechnen. Compose überwacht automatisch alle Zustände, die innerhalb dieses Blocks gelesen werden. Wenn sich einer dieser Eingabezustände ändert, führt Compose den Block erneut aus, um den neuen abgeleiteten Wert zu berechnen.
Der entscheidende Mechanismus für die Performance-Steigerung liegt jedoch im nächsten Schritt: Compose vergleicht das neu berechnete Ergebnis mit dem vorherigen Ergebnis. Nur wenn das neue Ergebnis von dem alten abweicht, werden die Composables, die diesen abgeleiteten Zustand lesen, für eine Recomposition markiert. Das zugrundeliegende State-Snapshot-System von Compose stellt sicher, dass diese Abhängigkeiten effizient verwaltet werden.
Man kann sich derivedStateOf wie ein Ventil vorstellen, das eine hohe Frequenz an Eingaben in eine niedrigere Frequenz an Ausgaben übersetzt. Ein klassisches Anti-Pattern in Compose ist es, eine teure Operation direkt in einer Composable-Funktion auszuführen, die von einem sich häufig ändernden Zustand abhängt. Jedes Mal, wenn sich der Zustand ändert, wird die Composable neu ausgeführt und die teure Berechnung wird wiederholt, selbst wenn das Endergebnis der Berechnung für den Nutzer gleich bleibt.
Ein weiterer wichtiger Aspekt ist das Caching. Das Ergebnis der Berechnung innerhalb von derivedStateOf wird zwischengespeichert. Solange sich die Eingabezustände nicht ändern, gibt Aufrufe des abgeleiteten Zustands einfach den zwischengespeicherten Wert zurück, ohne die Berechnung erneut auszuführen. Dies ist besonders wertvoll bei Berechnungen, die viel CPU-Zeit in Anspruch nehmen.
Um derivedStateOf effektiv zu nutzen, musst du verstehen, dass es nicht für jede Art von abgeleiteten Daten notwendig oder sinnvoll ist. Wenn sich der abgeleitete Wert genau so oft ändert wie der Ursprungszustand, bietet derivedStateOf keinen Performance-Vorteil. Im Gegenteil, es fügt sogar einen leichten Overhead durch die Verwaltung des Zustandsobjekts und den internen Wertevergleich hinzu. Die Magie entfaltet sich erst dann, wenn die Ausgabefrequenz deutlich geringer ist als die Eingabefrequenz. Ein häufiger Fehler ist die Annahme, dass derivedStateOf jeden Code schneller macht. Es ist ein Werkzeug für spezifische Probleme, nämlich dann, wenn du eine Entkopplung von Änderungsraten benötigst.
In der Praxis
Schauen wir uns konkrete Beispiele an, die in fast jeder modernen Android-App vorkommen. Das klassische Szenario ist eine Liste mit einem Button, der den Nutzer an den Anfang der Liste zurückspringen lässt. Dieser Button soll nur sichtbar sein, wenn der Nutzer ein Stück nach unten gescrollt hat.
Hier ist zunächst die ineffiziente Variante, die einen häufigen Fehler demonstriert:
@Composable
fun InefficientListWithScrollToTop(items: List<String>) {
val listState = rememberLazyListState()
// WARNUNG: Ineffizient!
// firstVisibleItemIndex ändert sich sehr häufig beim Scrollen.
// Das führt bei jedem Scroll-Event zu einer Recomposition.
val showButton = listState.firstVisibleItemIndex > 0
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState) {
items(items) { item ->
Text(text = item, modifier = Modifier.padding(16.dp))
}
}
if (showButton) {
FloatingActionButton(
onClick = { /* Logik zum Scrollen */ },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
) {
Icon(Icons.Default.ArrowUpward, contentDescription = "Nach oben")
}
}
}
}
In diesem Code liest die Composable InefficientListWithScrollToTop die Eigenschaft listState.firstVisibleItemIndex direkt. Da sich dieser Index beim Scrollen durch die Liste ständig ändert, triggert Compose für jede Änderung eine komplette Recomposition der gesamten Ansicht, selbst wenn der Wert von showButton von true auf true wechselt. Das ist reine Ressourcenverschwendung.
Die Lösung besteht darin, den Zustand über derivedStateOf abzuleiten:
@Composable
fun EfficientListWithScrollToTop(items: List<String>) {
val listState = rememberLazyListState()
// KORREKT: Effizient!
// Die Berechnung findet statt, wenn sich der Index ändert.
// Eine Recomposition wird aber nur ausgelöst, wenn showButton
// tatsächlich von false auf true (oder umgekehrt) wechselt.
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState) {
items(items) { item ->
Text(text = item, modifier = Modifier.padding(16.dp))
}
}
if (showButton) {
FloatingActionButton(
onClick = { /* Logik zum Scrollen */ },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
) {
Icon(Icons.Default.ArrowUpward, contentDescription = "Nach oben")
}
}
}
}
Durch die Verwendung von remember { derivedStateOf { ... } } wird die UI nur dann neu gezeichnet, wenn der Nutzer das erste Element aus dem Sichtfeld scrollt (Button erscheint) oder wenn er wieder ganz nach oben scrollt (Button verschwindet). Während des regulären Scrollens im mittleren Bereich der Liste bleibt der boolesche Wert unverändert auf true und es finden keine unnötigen Recompositions statt.
Eine typische Stolperfalle bei der Verwendung von derivedStateOf ist die Verwechslung mit remember(key). Wenn du beispielsweise eine Liste filtern möchtest und sich der Suchbegriff nur bei einem Klick auf einen “Suchen”-Button ändert, ist remember(query) { items.filter { ... } } die richtige Wahl. Das liegt daran, dass der Suchbegriff kein Zustandsobjekt ist, das sich extrem schnell ändert, und das Ergebnis der Filterung sich exakt so oft ändert wie der Suchbegriff.
Verwende derivedStateOf, wenn sich deine Eingabe häufig ändert (wie Scroll-Positionen oder Animations-Frames), deine Ausgabe sich aber selten ändert (wie ein Schwellenwert). Setze es nicht ein, wenn du nur zwei einfache Zustände kombinierst, wie Vor- und Nachname zu einem vollständigen Namen, da hier Eingabe- und Änderungsrate identisch sind. Ein solches Vorgehen würde den Code nur verkomplizieren, ohne die Performance der Applikation zu verbessern. Die bewusste Entscheidung für oder gegen diesen Mechanismus unterscheidet einen Anfänger von einem Junior-Developer, der Performance-Implikationen versteht.
Fazit
Zusammenfassend ist derivedStateOf ein unverzichtbares Werkzeug, um die Rendering-Performance deiner Jetpack Compose-Anwendungen auf einem hohen Niveau zu halten. Es hilft dir, teure Neuberechnungen zu isolieren und Recompositions nur dann zuzulassen, wenn sich entscheidende Werte für die Benutzeroberfläche geändert haben. Nutze den Layout Inspector in Android Studio, um die Recomposition-Zähler deiner App zu überwachen und Engpässe zu identifizieren. Schreibe zusätzlich automatisierte UI-Tests, die das Scroll-Verhalten und die Sichtbarkeit von dynamischen Elementen wie Buttons prüfen. So kannst du sicherstellen, dass deine Architektur-Entscheidungen nicht nur in der Theorie korrekt sind, sondern auch in der Praxis eine reibungslose Nutzererfahrung garantieren. Teste deine Compose-UIs systematisch, verifiziere Zustandsübergänge im Debugger und integriere Performance-Prüfungen in deine CI-Pipeline, um Regressionen frühzeitig zu erkennen.