Recomposition in Jetpack Compose: Grundlagen und Funktionsweise
Erfahre, was Recomposition in Jetpack Compose ist, warum sie nicht schlecht ist und wie du UI-Updates in Android-Apps gezielt steuerst und optimierst.
Jetpack Compose hat die Art und Weise verändert, wie wir Android-Oberflächen bauen. Anstatt Ansichten manuell zu modifizieren, beschreibst du den Zustand deiner Benutzeroberfläche deklarativ. Ändert sich dieser Zustand, muss das Framework reagieren und die betroffenen Teile der UI aktualisieren. Dieser Mechanismus bildet das Herzstück der modernen Android-Entwicklung und ist essenziell für flüssige, reaktionsschnelle Anwendungen. Wer diesen Prozess versteht, vermeidet Performance-Probleme und schreibt sauberen, wartbaren Code.
Was ist das?
Recomposition ist der Kernmechanismus von Jetpack Compose, der dafür sorgt, dass deine Benutzeroberfläche immer den aktuellen Datenstatus widerspiegelt. In herkömmlichen Android-Apps mit XML hast du UI-Elemente direkt über Methoden wie setText() oder setVisibility() verändert. In Compose rufst du stattdessen Composable-Funktionen auf und übergibst ihnen Daten. Wenn sich diese Daten ändern, ruft das Framework die entsprechenden Funktionen erneut mit den neuen Parametern auf. Dieser Vorgang des erneuten Aufrufens wird als Recomposition bezeichnet.
Ein weit verbreiteter Irrtum unter Einsteigern ist die Annahme, dass Recomposition grundsätzlich vermieden werden muss, weil sie Ressourcen verbraucht. Das ist falsch. Recomposition ist das erwartete Verhalten und an sich nichts Schlechtes. Sie ist der einzige Weg, wie eine deklarative UI auf neue Zustände reagieren kann. Das Framework ist stark darauf optimiert, diese Updates schnell und effizient durchzuführen. Es erkennt automatisch, welche Teile der Oberfläche von einer Datenänderung betroffen sind, und aktualisiert nur diese spezifischen Bereiche. Dieser Vorgang nennt sich Invalidation. Wenn eine Zustandsänderung registriert wird, markiert Compose die betroffenen Scopes als ungültig (invalidated) und plant sie für den nächsten Frame zur Recomposition ein.
Gleichzeitig wendet das System eine Technik namens Skipping an. Wenn Compose bei der Auswertung einer Funktion feststellt, dass sich die Eingabeparameter im Vergleich zum vorherigen Aufruf nicht verändert haben, wird die Ausführung dieser spezifischen Funktion übersprungen. Die bisherige UI-Darstellung bleibt erhalten. Dieses Zusammenspiel aus Invalidation und Skipping ermöglicht es dir, komplexe Oberflächen zu erstellen, ohne dich um die manuelle Aktualisierung einzelner Ansichten kümmern zu müssen.
Wie funktioniert es?
Um die Mechanik hinter diesem Prozess zu verstehen, musst du wissen, wie Compose intern arbeitet. Wenn eine Composable-Funktion zum ersten Mal ausgeführt wird, baut das System eine interne Baumstruktur auf, die alle UI-Elemente und deren Abhängigkeiten von Zustandsvariablen (State) speichert. Diese Phase wird Initial Composition genannt. Jedes Mal, wenn ein State-Objekt gelesen wird, registriert Compose automatisch, welche Composable-Funktion diesen Wert verwendet.
Ändert sich nun der Wert dieses State-Objekts, tritt Invalidation ein. Das Framework weiß durch die vorherige Registrierung genau, welche Funktionen von diesem bestimmten Zustand abhängen. Es plant diese Funktionen für eine Recomposition ein. Alle anderen Funktionen im Baum, die diesen spezifischen Zustand nicht verwenden, bleiben von der Invalidation unberührt.
Sobald der nächste Frame gerendert wird, startet die Recomposition. Compose durchläuft den Baum ab den markierten Stellen nach unten. Bevor eine Funktion jedoch tatsächlich ausgeführt wird, prüft das System ihre Parameter. Hier kommt das Skipping ins Spiel. Damit Compose eine Funktion überspringen kann, müssen zwei Bedingungen erfüllt sein: Die Funktion muss als “restartable” gelten (was auf fast alle Composables zutrifft), und alle Parameter müssen “stable” sein.
Ein Typ gilt als stable, wenn Compose garantieren kann, dass Änderungen an seinen Eigenschaften zuverlässig bemerkt werden. Primitive Datentypen wie Int, String oder Boolean sind von Natur aus stable. Auch Klassen, deren Eigenschaften alle als val deklariert sind und nur stable Typen enthalten, gelten als stable. Wenn du jedoch eine Klasse verwendest, die veränderliche Eigenschaften (mutables) wie ein var enthält oder auf Instanzen verweist, die sich ohne das Wissen von Compose ändern können, stuft das System diesen Typ als unstable ein.
Wenn eine Composable-Funktion unstable Parameter hat, muss Compose davon ausgehen, dass sich die Werte geändert haben könnten, selbst wenn die Referenz auf das Objekt gleich geblieben ist. In diesem Fall wird das Skipping deaktiviert, und die Funktion wird bei jeder Recomposition ihres Eltern-Elements neu ausgeführt. Das kann zu Leistungseinbußen führen, wenn betroffene Funktionen aufwendige Berechnungen durchführen oder tief verschachtelte Layouts enthalten. Daher ist das Verständnis von Stabilität entscheidend für die Optimierung deiner Apps.
In der Praxis
Im Entwickleralltag begegnet dir dieses Thema vor allem dann, wenn deine App träge reagiert oder UI-Elemente unerwartet flackern. Ein typisches Szenario ist die Verwendung von Listen mit komplexen Datensätzen. Stell dir vor, du hast eine Liste von Benutzern, und du möchtest den Namen eines Benutzers aktualisieren.
Hier ist ein einfaches, aber fehleranfälliges Beispiel:
data class User(var name: String, val age: Int)
@Composable
fun UserProfile(user: User) {
Text(text = "Name: ${user.name}")
Text(text = "Alter: ${user.age}")
}
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
UserProfile(user = user)
}
}
}
In diesem Fall ist die Klasse User unstable, weil die Eigenschaft name als var deklariert ist. Selbst wenn sich nur der Name eines einzigen Benutzers ändert und du die Liste neu zeichnest, kann Compose das Skipping für UserProfile nicht anwenden. Das Framework muss alle UserProfile-Funktionen neu ausführen, da es nicht garantieren kann, ob sich die Werte der anderen User-Objekte im Hintergrund verändert haben.
Die Lösung besteht darin, unveränderliche Datenstrukturen zu verwenden. Wenn du User als unveränderlich definierst, wird die Klasse stable:
data class User(val name: String, val age: Int)
@Composable
fun UserProfile(user: User) {
Text(text = "Name: ${user.name}")
Text(text = "Alter: ${user.age}")
}
Wenn du nun den Namen eines Benutzers ändern möchtest, erstellst du eine neue Instanz von User (zum Beispiel mit der copy()-Methode) und aktualisierst die Liste im State. Compose erkennt, dass sich die Referenz des Objekts für diesen spezifischen Benutzer geändert hat, und führt eine Recomposition nur für dieses eine UserProfile durch. Für alle anderen Benutzer in der Liste sind die Objektreferenzen gleich geblieben. Da User nun stable ist, wendet Compose das Skipping an und überspringt die Ausführung der anderen UserProfile-Funktionen.
Eine typische Stolperfalle ist die Verwendung von Standard-Collections wie List, Set oder Map aus der Kotlin-Standardbibliothek. Aus Sicht von Compose sind diese Collections unstable, da sie in Kotlin Interfaces sind und von veränderlichen Implementierungen (wie ArrayList) unterstützt werden könnten. Um dieses Problem zu lösen, kannst du unveränderliche Collections aus der Bibliothek kotlinx.collections.immutable verwenden, wie ImmutableList. Alternativ kannst du die Klasse, die die Liste hält, mit der Annotation @Immutable oder @Stable markieren, um Compose zu signalisieren, dass du dich an die Regeln hältst und die Liste nach der Erstellung nicht mehr veränderst.
Um Probleme zu identifizieren, bietet Android Studio das Tool “Layout Inspector”. Damit kannst du verfolgen, wie oft Composables neu gezeichnet werden und ob bestimmte Elemente unerwartet oft eine Recomposition durchlaufen. Diese Metriken helfen dir, Engpässe zu finden und deine Parameter zu optimieren.
Fazit
Recomposition ist das fundamentale Konzept, mit dem Jetpack Compose deine Benutzeroberfläche an Datenänderungen anpasst. Sie arbeitet durch Invalidation von betroffenen Bereichen und überspringt unveränderte Teile durch Skipping, sofern die Datenstrukturen als stabil erkannt werden. Du kannst dein Verständnis für diesen Prozess am besten festigen, indem du in deinen eigenen Projekten bewusst auf die Stabilität deiner Datenklassen achtest. Nutze den Layout Inspector in Android Studio, um die Recomposition-Zähler in deiner App zu beobachten, und prüfe, ob deine Optimierungen den gewünschten Effekt erzielen. So stellst du sicher, dass deine UIs nicht nur korrekt, sondern auch hochgradig performant bleiben.