Wie die meisten anderen UI-Toolkits rendert Compose einen Frame in mehreren unterschiedlichen Phasen. Das Android-View-System hat beispielsweise drei Hauptphasen: Messen, Layout und Zeichnen. Compose ist sehr ähnlich, hat aber zu Beginn eine wichtige zusätzliche Phase namens Zusammensetzung.
In der Compose-Dokumentation wird die Komposition unter Thinking in Compose und State and Jetpack Compose beschrieben.
Die drei Phasen eines Frames
Compose besteht aus drei Hauptphasen:
- Zusammensetzung: Welche Benutzeroberfläche soll angezeigt werden? Compose führt zusammensetzbare Funktionen aus und erstellt eine Beschreibung Ihrer Benutzeroberfläche.
- Layout: Wo die Benutzeroberfläche platziert werden soll. Diese Phase besteht aus zwei Schritten: Analyse und Platzierung. Layout-Elemente werden in 2D-Koordinaten gemessen und platziert, ebenso alle untergeordneten Elemente, und zwar für jeden Knoten im Layoutbaum.
- Zeichnung: So wird sie gerendert. UI-Elemente werden in einem Canvas gerendert, in der Regel auf einem Gerätebildschirm.

Die Reihenfolge dieser Phasen ist in der Regel dieselbe, sodass Daten in eine Richtung fließen können, von der Komposition über das Layout bis hin zum Rendern eines Frames (auch als unidirektionaler Datenfluss bezeichnet). BoxWithConstraints
, LazyColumn
} und LazyRow
sind bemerkenswerte Ausnahmen, bei denen die Zusammensetzung der untergeordneten Elemente von der Layoutphase des übergeordneten Elements abhängt.
Konzeptionell durchläuft jeder Frame alle diese Phasen. Um die Leistung zu optimieren, vermeidet Compose jedoch, Arbeit zu wiederholen, die in allen Phasen dieselben Ergebnisse aus denselben Eingaben berechnen würde. Compose überspringt die Ausführung einer zusammensetzbaren Funktion, wenn ein früheres Ergebnis wiederverwendet werden kann. Außerdem wird das gesamte Compose UI nicht neu angeordnet oder neu gezeichnet, wenn das nicht erforderlich ist. Compose führt nur die Mindestanzahl an Vorgängen aus, die zum Aktualisieren der Benutzeroberfläche erforderlich sind. Diese Optimierung ist möglich, weil Compose den Status in den verschiedenen Phasen verfolgt.
Phasen verstehen
In diesem Abschnitt wird genauer beschrieben, wie die drei Compose-Phasen für Composables ausgeführt werden.
Komposition
In der Kompositionsphase führt die Compose-Laufzeit zusammensetzbare Funktionen aus und gibt eine Baumstruktur aus, die Ihre Benutzeroberfläche darstellt. Dieser UI-Baum besteht aus Layoutknoten, die alle Informationen enthalten, die für die nächsten Phasen erforderlich sind, wie im folgenden Video gezeigt:
Abbildung 2: Der Baum, der Ihre Benutzeroberfläche darstellt und in der Kompositionsphase erstellt wird.
Ein Unterabschnitt des Code- und UI-Baums sieht so aus:

In diesen Beispielen wird jede zusammensetzbare Funktion im Code einem einzelnen Layoutknoten im UI-Baum zugeordnet. In komplexeren Beispielen können Composables Logik und Kontrollfluss enthalten und je nach Status einen anderen Baum erzeugen.
Layout
In der Layoutphase verwendet Compose den in der Kompositionsphase erstellten UI-Baum als Eingabe. Die Sammlung von Layoutknoten enthält alle Informationen, die erforderlich sind, um die Größe und Position jedes Knotens im 2D-Raum zu bestimmen.
Abbildung 4: Die Messung und Platzierung jedes Layoutknotens im UI-Baum während der Layoutphase.
In der Layoutphase wird der Baum mit dem folgenden dreistufigen Algorithmus durchlaufen:
- Untergeordnete Elemente messen: Ein Knoten misst seine untergeordneten Elemente, sofern vorhanden.
- Eigene Größe festlegen: Anhand dieser Messungen legt ein Knoten seine eigene Größe fest.
- Untergeordnete Elemente platzieren: Jeder untergeordnete Knoten wird relativ zur Position eines Knotens platziert.
Am Ende dieser Phase hat jeder Layoutknoten Folgendes:
- Eine zugewiesene Breite und Höhe
- Eine x- und y-Koordinate, an der sie gezeichnet werden soll
Erinnern Sie sich an den UI-Baum aus dem vorherigen Abschnitt:
Für diesen Baum funktioniert der Algorithmus so:
- Die
Row
misst die untergeordneten KnotenImage
undColumn
. - Die
Image
wird gemessen. Es hat keine untergeordneten Elemente, daher wird die Größe selbst bestimmt und anRow
zurückgemeldet. - Als Nächstes wird der
Column
gemessen. Die Größe der eigenen untergeordneten Elemente (zweiText
-Composables) wird zuerst gemessen. - Die erste
Text
wird gemessen. Da es keine untergeordneten Elemente hat, bestimmt es seine eigene Größe und meldet sie anColumn
zurück.- Die zweite
Text
wird gemessen. Es hat keine untergeordneten Elemente, daher bestimmt es seine eigene Größe und meldet sie anColumn
zurück.
- Die zweite
- Die
Column
richtet sich bei der Größenbestimmung nach den Maßen des Kindes. Dabei wird die maximale Breite des untergeordneten Elements und die Summe der Höhe der untergeordneten Elemente verwendet. - Das
Column
positioniert seine untergeordneten Elemente relativ zu sich selbst und platziert sie vertikal untereinander. - Die
Row
richtet sich bei der Größenbestimmung nach den Maßen des Kindes. Dabei werden die maximale Höhe des untergeordneten Elements und die Summe der Breiten der untergeordneten Elemente verwendet. Anschließend werden die untergeordneten Elemente platziert.
Beachten Sie, dass jeder Knoten nur einmal besucht wurde. Für die Compose-Laufzeit ist nur ein Durchlauf des UI-Baums erforderlich, um alle Knoten zu messen und zu platzieren. Das verbessert die Leistung. Wenn die Anzahl der Knoten im Baum zunimmt, steigt die Zeit, die für das Durchlaufen des Baums benötigt wird, linear an. Wenn jeder Knoten mehrmals besucht wird, erhöht sich die Durchlaufzeit exponentiell.
Zeichnen
In der Zeichenphase wird der Baum noch einmal von oben nach unten durchlaufen und jeder Knoten zeichnet sich nacheinander auf dem Bildschirm.
Abbildung 5: In der Zeichenphase werden die Pixel auf dem Bildschirm dargestellt.
Im vorherigen Beispiel wird der Inhalt des Baums so dargestellt:
- Das
Row
rendert alle Inhalte, die es enthält, z. B. eine Hintergrundfarbe. - Die
Image
wird automatisch gezeichnet. - Die
Column
wird automatisch gezeichnet. - Die erste und zweite
Text
werden jeweils selbst gezeichnet.
Abbildung 6 Ein UI-Baum und seine gerenderte Darstellung.
Statuslesevorgänge
Wenn Sie die value
eines snapshot state
während einer der oben aufgeführten Phasen lesen, verfolgt Compose automatisch, was es beim Lesen der value
getan hat. Durch dieses Tracking kann Compose den Reader neu ausführen, wenn sich der Status von value
ändert. Das ist die Grundlage für die Statusbeobachtung in Compose.
Der Status wird in der Regel mit mutableStateOf()
erstellt und dann auf zwei Arten darauf zugegriffen: entweder direkt über die value
-Property oder alternativ über einen Kotlin-Property-Delegate. Weitere Informationen dazu finden Sie unter Status in Composables. In diesem Leitfaden bezieht sich „Status lesen“ auf eine dieser beiden gleichwertigen Zugriffsmethoden.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
Unter der Haube des Eigenschaftendelegaten werden „Getter“- und „Setter“-Funktionen verwendet, um auf das value
des Status zuzugreifen und es zu aktualisieren. Diese Getter- und Setter-Funktionen werden nur aufgerufen, wenn Sie auf die Eigenschaft als Wert verweisen, nicht wenn sie erstellt wird. Daher sind die beiden oben beschriebenen Methoden gleichwertig.
Jeder Codeblock, der bei einer Änderung des Lesestatus neu ausgeführt werden kann, ist ein Neustartbereich. Compose verfolgt Statusänderungen value
und startet Bereiche in verschiedenen Phasen neu.
Phasengesteuerte Statuslesevorgänge
Wie bereits erwähnt, gibt es in Compose drei Hauptphasen. Compose verfolgt, welcher Status in jeder Phase gelesen wird. So kann Compose nur die spezifischen Phasen benachrichtigen, die für jedes betroffene Element Ihrer Benutzeroberfläche Arbeit leisten müssen.
In den folgenden Abschnitten werden die einzelnen Phasen beschrieben und es wird erläutert, was passiert, wenn in einer Phase ein Statuswert gelesen wird.
Phase 1: Komposition
Statuslesevorgänge in einer @Composable
-Funktion oder einem Lambda-Block wirken sich auf die Zusammensetzung und möglicherweise auf die nachfolgenden Phasen aus. Wenn sich der value
des Status ändert, plant der Recomposer die erneute Ausführung aller zusammensetzbaren Funktionen, die den value
dieses Status lesen. Die Laufzeit kann entscheiden, einige oder alle zusammensetzbaren Funktionen zu überspringen, wenn sich die Eingaben nicht geändert haben. Weitere Informationen finden Sie unter Überspringen, wenn sich die Eingaben nicht geändert haben.
Abhängig vom Ergebnis der Komposition führt Compose UI die Layout- und Zeichenphasen aus. Diese Phasen werden möglicherweise übersprungen, wenn der Inhalt gleich bleibt und sich Größe und Layout nicht ändern.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Phase 2: Layout
Die Layoutphase besteht aus zwei Schritten: Messung und Platzierung. Im Messschritt wird unter anderem die Measure-Lambda-Funktion ausgeführt, die an die zusammensetzbare Funktion Layout
übergeben wird, sowie die Methode MeasureScope.measure
der Schnittstelle LayoutModifier
.
Im Platzierungsschritt wird der Platzierungsblock der Funktion layout
, der Lambda-Block von Modifier.offset { … }
und ähnliche Funktionen ausgeführt.
Das Lesen des Status in jedem dieser Schritte wirkt sich auf das Layout und möglicherweise auf die Zeichenphase aus. Wenn sich der value
des Status ändert, plant Compose UI die Layoutphase. Außerdem wird die Zeichenphase ausgeführt, wenn sich die Größe oder Position geändert hat.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Phase 3: Ziehen
Statuslesevorgänge während des Zeichencodes wirken sich auf die Zeichenphase aus. Gängige Beispiele sind Canvas()
, Modifier.drawBehind
und Modifier.drawWithContent
. Wenn sich der value
des Status ändert, wird in der Compose-Benutzeroberfläche nur die Zeichenphase ausgeführt.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Statuslesevorgänge optimieren
Da Compose das Lesen von lokalisiertem Status verfolgt, können Sie die Menge der Arbeit minimieren, indem Sie jeden Status in einer geeigneten Phase lesen.
Dazu ein Beispiel: In diesem Beispiel gibt es ein Image()
, für das der Offset-Modifikator verwendet wird, um die endgültige Layoutposition zu verschieben. Dadurch entsteht beim Scrollen ein Parallaxeneffekt.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Dieser Code funktioniert, führt aber zu einer suboptimalen Leistung. Wie geschrieben, liest der Code die value
des firstVisibleItemScrollOffset
-Zustands und übergibt sie an die Funktion Modifier.offset(offset: Dp)
. Während der Nutzer scrollt, ändert sich die value
der firstVisibleItemScrollOffset
. Wie Sie bereits gelernt haben, verfolgt Compose alle Statuslesevorgänge, damit der Lesecode neu gestartet (neu aufgerufen) werden kann. In diesem Beispiel ist das der Inhalt von Box
.
Dies ist ein Beispiel für das Lesen eines Status in der Phase composition. Das ist nicht unbedingt schlecht, sondern die Grundlage für die Neuzusammensetzung, die es ermöglicht, dass durch Datenänderungen neue UI-Elemente ausgegeben werden.
Wichtiger Punkt: Dieses Beispiel ist suboptimal, da jedes Scroll-Ereignis dazu führt, dass der gesamte zusammensetzbare Inhalt neu ausgewertet, gemessen, angeordnet und schließlich gezeichnet wird. Sie lösen die Compose-Phase bei jedem Scrollen aus, obwohl sich der angezeigte Inhalt nicht geändert hat, sondern nur seine Position. Sie können das Lesen des Status optimieren, sodass nur die Layoutphase neu ausgelöst wird.
Offset mit Lambda
Es ist eine weitere Version des Offset-Modifiers verfügbar:
Modifier.offset(offset: Density.() -> IntOffset)
.
Diese Version verwendet einen Lambda-Parameter, wobei der resultierende Offset vom Lambda-Block zurückgegeben wird. So aktualisieren Sie den Code, um ihn zu verwenden:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Warum ist das leistungsfähiger? Der Lambda-Block, den Sie für den Modifier bereitstellen, wird während der Layoutphase aufgerufen, genauer gesagt während des Platzierungsschritts der Layoutphase. Das bedeutet, dass der firstVisibleItemScrollOffset
-Zustand während der Komposition nicht mehr gelesen wird. Da Compose nachverfolgt, wann der Status gelesen wird, muss Compose bei einer Änderung von value
der firstVisibleItemScrollOffset
nur die Layout- und Zeichenphasen neu starten.
Natürlich ist es oft absolut notwendig, Zustände in der Kompositionsphase zu lesen. Trotzdem gibt es Fälle, in denen Sie die Anzahl der Neuzusammensetzungen minimieren können, indem Sie Statusänderungen filtern. Weitere Informationen dazu finden Sie unter derivedStateOf
: Ein oder mehrere Statusobjekte in einen anderen Status konvertieren.
Schleife für die Neuzusammensetzung (zyklische Phasenabhängigkeit)
In diesem Leitfaden wurde zuvor erwähnt, dass die Phasen von Compose immer in derselben Reihenfolge aufgerufen werden und dass es keine Möglichkeit gibt, innerhalb desselben Frames zurückzugehen. Das verhindert jedoch nicht, dass Apps in Kompositionsloops über verschiedene Frames hinweg geraten. Betrachten wir dieses Beispiel:
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
In diesem Beispiel wird eine vertikale Spalte mit dem Bild oben und dem Text darunter implementiert. Dazu wird Modifier.onSizeChanged()
verwendet, um die aufgelöste Größe des Bildes abzurufen, und dann Modifier.padding()
für den Text, um ihn nach unten zu verschieben.
Die unnatürliche Konvertierung von Px
zurück zu Dp
deutet bereits auf ein Problem im Code hin.
Das Problem bei diesem Beispiel ist, dass der Code nicht in einem einzigen Frame zum „finalen“ Layout gelangt. Der Code basiert auf mehreren Frames, was unnötige Arbeit verursacht und dazu führt, dass die Benutzeroberfläche auf dem Bildschirm für den Nutzer hin- und herspringt.
Zusammensetzung des ersten Frames
Während der Kompositionsphase des ersten Frames ist imageHeightPx
anfangs 0
. Der Code gibt den Text daher mit Modifier.padding(top = 0)
aus.
In der nachfolgenden Layoutphase wird der Callback des onSizeChanged
-Modifiers aufgerufen, wodurch imageHeightPx
auf die tatsächliche Höhe des Bildes aktualisiert wird. „Compose“ plant dann eine Neukomposition für den nächsten Frame. Während der aktuellen Zeichenphase wird der Text jedoch mit einem Padding von 0
gerendert, da der aktualisierte imageHeightPx
-Wert noch nicht berücksichtigt wird.
Zweite Frame-Komposition
Mit „Compose“ wird der zweite Frame initiiert, der durch die Änderung des Werts von imageHeightPx
ausgelöst wird. In der Kompositionsphase dieses Frames wird der Status im Inhaltsblock Box
gelesen. Der Text wird jetzt mit einem Innenabstand versehen, der genau der Höhe des Bildes entspricht. Während der Layoutphase wird imageHeightPx
noch einmal festgelegt. Es wird jedoch keine weitere Neuzusammenstellung geplant, da der Wert gleich bleibt.
Dieses Beispiel mag gekünstelt erscheinen, aber Vorsicht vor diesem allgemeinen Muster:
Modifier.onSizeChanged()
,onGloballyPositioned()
oder andere Layoutvorgänge- Zustand aktualisieren
- Verwenden Sie diesen Status als Eingabe für einen Layoutmodifikator (
padding()
,height()
oder ähnlich). - Möglicherweise wiederholen
Die Lösung für das vorherige Beispiel besteht darin, die richtigen Layout-Grundelemente zu verwenden. Das vorherige Beispiel kann mit einem Column()
implementiert werden. Möglicherweise haben Sie aber ein komplexeres Beispiel, das etwas Benutzerdefiniertes erfordert. In diesem Fall müssen Sie ein benutzerdefiniertes Layout schreiben. Weitere Informationen finden Sie im Leitfaden Benutzerdefinierte Layouts.
Das allgemeine Prinzip besteht darin, eine einzige Quelle für mehrere UI-Elemente zu haben, die in Bezug zueinander gemessen und platziert werden sollen. Wenn Sie ein geeignetes Layout-Primitive verwenden oder ein benutzerdefiniertes Layout erstellen, dient das minimale gemeinsame übergeordnete Element als Source of Truth, mit der die Beziehung zwischen mehreren Elementen koordiniert werden kann. Die Einführung eines dynamischen Status verstößt gegen diesen Grundsatz.
Empfehlungen für dich
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Zustand und Jetpack Compose
- Listen und Tabellen
- Kotlin für Jetpack Compose