Wie bei den 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 Komposition.
In der Compose-Dokumentation werden Komposition in Thinking in Compose und State and Jetpack Compose beschrieben.
Die drei Phasen eines Frames
Compose hat drei Hauptphasen:
- Komposition: Welche UI soll angezeigt werden? Compose führt zusammensetzbare Funktionen aus und erstellt eine Beschreibung der UI.
- Layout: Wo soll die UI platziert werden? Diese Phase besteht aus zwei Schritten: Messen und Platzieren. Layout-Elemente messen und platzieren sich selbst und alle untergeordneten Elemente in 2D-Koordinaten für jeden Knoten im Layout-Baum.
- Zeichnen: Wie wird gerendert? UI-Elemente werden in einem Canvas gezeichnet, in der Regel auf einem Gerätebildschirm.
Die Reihenfolge dieser Phasen ist in der Regel gleich, sodass Daten in einer
Richtung von der Komposition über das Layout zum Zeichnen fließen können, um einen Frame zu erstellen (auch als
unidirektionaler Datenfluss bezeichnet). BoxWithConstraints, LazyColumn,
und LazyRow sind bemerkenswerte Ausnahmen, bei denen die Komposition der untergeordneten Elemente
von der Layout-Phase des übergeordneten Elements abhängt.
Konzeptionell durchläuft jeder Frame jede dieser Phasen. Um die Leistung zu optimieren, vermeidet Compose jedoch, Arbeit zu wiederholen, bei der in allen diesen Phasen dieselben Ergebnisse aus denselben Eingaben berechnet würden. Compose überspringt die Ausführung einer zusammensetzbaren Funktion, wenn ein früheres Ergebnis wiederverwendet werden kann. Die Compose-UI führt kein neues Layout oder Zeichnen des gesamten Baums aus, wenn dies nicht erforderlich ist. Compose führt nur die Mindestmenge an Arbeit aus, die zum Aktualisieren der UI erforderlich ist. Diese Optimierung ist möglich, weil Compose Statuslesevorgänge in den verschiedenen Phasen verfolgt.
Die Phasen
In diesem Abschnitt wird beschrieben, wie die drei Compose-Phasen für zusammensetzbare Elemente ausgeführt werden.
Komposition
In der Kompositionsphase führt die Compose-Laufzeit zusammensetzbare Funktionen aus und gibt eine Baumstruktur aus, die die UI darstellt. Dieser UI-Baum besteht aus Layout-Knoten, die alle Informationen enthalten, die für die nächsten Phasen erforderlich sind, wie im folgenden Video gezeigt:
Abbildung 2 : Der Baum, der die UI 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 Layout-Knoten im UI-Baum zugeordnet. In komplexeren Beispielen können zusammensetzbare Elemente Logik und Kontrollfluss enthalten und je nach Status einen anderen Baum erzeugen.
Layout
In der Layout-Phase verwendet Compose den in der Kompositionsphase erstellten UI-Baum als Eingabe. Die Sammlung von Layout-Knoten enthält alle Informationen, die erforderlich sind, um die Größe und Position jedes Knotens im 2D-Raum zu bestimmen.
Abbildung 4 : Messen und Platzieren jedes Layout-Knotens im UI-Baum während der Layout-Phase.
Während der Layout-Phase wird der Baum mit dem folgenden dreistufigen Algorithmus durchlaufen:
- Untergeordnete Elemente messen: Ein Knoten misst seine untergeordneten Elemente, falls vorhanden.
- Eigene Größe bestimmen: Basierend auf diesen Messungen bestimmt ein Knoten seine eigene Größe.
- Untergeordnete Elemente platzieren: Jeder untergeordnete Knoten wird relativ zur Position eines Knotens platziert.
Am Ende dieser Phase hat jeder Layout-Knoten Folgendes:
- Eine zugewiesene Breite und Höhe
- Eine x-, y-Koordinate , an der er gezeichnet werden soll
Sehen Sie sich den UI-Baum aus dem vorherigen Abschnitt an:
Für diesen Baum funktioniert der Algorithmus so:
- Die
Rowmisst ihre untergeordneten ElementeImageundColumn. - Das
Imagewird gemessen. Es hat keine untergeordneten Elemente, bestimmt also seine eigene Größe und meldet sie an dieRowzurück. - Als Nächstes wird die
Columngemessen. Zuerst werden die untergeordneten Elemente (zwei zusammensetzbareText-Elemente) gemessen. - Das erste
Text-Element wird gemessen. Es hat keine untergeordneten Elemente, bestimmt also seine eigene Größe und meldet sie an dieColumnzurück.- Das zweite
Text-Element wird gemessen. Es hat keine untergeordneten Elemente, bestimmt also seine eigene Größe und meldet sie an dieColumnzurück.
- Das zweite
- Die
Columnbestimmt ihre eigene Größe anhand der Messungen der untergeordneten Elemente. Dazu verwendet sie die maximale Breite des untergeordneten Elements und die Summe der Höhen der untergeordneten Elemente. - Die
Columnplatziert ihre untergeordneten Elemente relativ zu sich selbst und untereinander vertikal. - Die
Rowbestimmt ihre eigene Größe anhand der Messungen der untergeordneten Elemente. Dazu verwendet sie die maximale Höhe des untergeordneten Elements und die Summe der Breiten der untergeordneten Elemente. Anschließend werden die untergeordneten Elemente platziert.
Jeder Knoten wurde nur einmal besucht. Die Compose-Laufzeit benötigt nur einen Durchlauf durch den UI-Baum, um alle Knoten zu messen und zu platzieren, was die Leistung verbessert. Wenn die Anzahl der Knoten im Baum steigt, steigt die Zeit für das Durchlaufen linear. Wenn jeder Knoten mehrmals besucht würde, würde die Durchlaufzeit exponentiell ansteigen.
Zeichnen
In der Zeichnungsphase wird der Baum noch einmal von oben nach unten durchlaufen und jeder Knoten zeichnet sich nacheinander auf dem Bildschirm.
Abbildung 5 : In der Zeichnungsphase werden die Pixel auf dem Bildschirm gezeichnet.
Im vorherigen Beispiel wird der Inhalt des Baums so gezeichnet:
- Die
Rowzeichnet alle Inhalte, die sie möglicherweise hat, z. B. eine Hintergrundfarbe. - Das
Imagezeichnet sich selbst. - Die
Columnzeichnet sich selbst. - Das erste und zweite
Text-Element zeichnen sich jeweils selbst.
Abbildung 6 : Ein UI-Baum und seine gezeichnete Darstellung.
Statuslesevorgänge
Wenn Sie den value eines snapshot state während einer der zuvor aufgeführten Phasen
lesen, verfolgt Compose automatisch, was beim Lesen
des value ausgeführt wurde. Durch diese Verfolgung kann Compose den Leser neu ausführen, wenn sich der value des Status ändert. Sie ist die Grundlage für die Statusbeobachtbarkeit in Compose.
In der Regel erstellen Sie den Status mit mutableStateOf() und greifen dann auf eine von zwei Arten darauf zu: entweder direkt über die Eigenschaft value oder über einen Kotlin-Eigenschafts-Delegaten. Weitere Informationen finden Sie unter Status in
zusammensetzbaren Elementen. In diesem Leitfaden bezieht sich „Statuslesevorgang“ auf eine dieser 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 Eigenschafts-Delegaten werden Getter- und Setter-
Funktionen verwendet, um auf den value des Status zuzugreifen und ihn zu aktualisieren. Diese Getter- und Setter-Funktionen werden nur aufgerufen, wenn Sie auf die Eigenschaft als Wert verweisen, und nicht, wenn sie erstellt wird. Deshalb sind die beiden zuvor beschriebenen Methoden gleichwertig.
Jeder Codeblock, der bei einer Änderung des gelesenen Status noch einmal ausgeführt werden kann, ist ein Neustartbereich. Compose verfolgt Änderungen des value des Status und Neustartbereiche in verschiedenen Phasen.
Phasenspezifische 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 der UI Arbeit ausführen müssen.
In den folgenden Abschnitten werden die einzelnen Phasen beschrieben und was passiert, wenn ein Statuswert darin gelesen wird.
Phase 1: Komposition
Statuslesevorgänge in einer @Composable-Funktion oder einem Lambda-Block wirken sich auf die Komposition 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 jedoch 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.
Je nach Ergebnis der Komposition führt die Compose-UI die Layout- und Zeichnungsphasen aus. Diese Phasen können übersprungen werden, 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 Layout-Phase besteht aus zwei Schritten: Messen und Platzieren. Im
Schritt „Messen“ wird unter anderem das Mess-Lambda ausgeführt, das an das Layout zusammensetzbare Element übergeben wurde, sowie die
MeasureScope.measure Methode der LayoutModifier Schnittstelle.
Im Schritt „Platzieren“ wird der Platzierungsblock der Funktion layout, der Lambda-Block von Modifier.offset { … } und ähnliche Funktionen ausgeführt.
Statuslesevorgänge während dieser Schritte wirken sich auf das Layout und möglicherweise auf die Zeichnungsphase aus. Wenn sich der value des Status ändert, plant die Compose-UI die Layout-Phase. Außerdem wird die Zeichnungsphase ausgeführt, wenn sich Größe oder Position geändert haben.
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: Zeichnen
Statuslesevorgänge während des Zeichnens wirken sich auf die Zeichnungsphase aus. Häufige Beispiele sind Canvas(), Modifier.drawBehind und Modifier.drawWithContent. Wenn sich der value des Status ändert, führt die Compose-UI nur die Zeichnungsphase aus.
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 die Statuslesevorgänge lokal verfolgt, können Sie den Arbeitsaufwand minimieren, indem Sie jeden Status in einer geeigneten Phase lesen.
Dazu ein Beispiel: Dieses Beispiel enthält ein Image(), das den Offset-Modifikator verwendet, um die endgültige Layout-Position zu verschieben. So entsteht ein Parallax-Effekt, wenn der Nutzer scrollt.
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
den value des Status firstVisibleItemScrollOffset und übergibt ihn an
die Funktion Modifier.offset(offset: Dp). Wenn der Nutzer scrollt, ändert sich der value von firstVisibleItemScrollOffset. Wie Sie bereits gelernt haben, verfolgt Compose alle Statuslesevorgänge, damit der Lesecode neu gestartet (noch einmal aufgerufen) werden kann. In diesem Beispiel ist das der Inhalt von Box.
Dies ist ein Beispiel für das Lesen eines Status in der Kompositionsphase. Das ist nicht unbedingt schlecht und ist sogar die Grundlage für die Neuzusammensetzung, da Datenänderungen neue UI-Elemente erzeugen können.
Wichtiger Hinweis: Dieses Beispiel ist suboptimal, weil jedes Scrollereignis dazu führt, dass der gesamte zusammensetzbare Inhalt neu bewertet, 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 den Statuslesevorgang optimieren, um nur die Layout-Phase noch einmal auszulösen.
Offset mit Lambda
Es gibt eine weitere Version des Offset-Modifikators:
Modifier.offset(offset: Density.() -> IntOffset).
Diese Version verwendet einen Lambda-Parameter, wobei der resultierende Offset vom Lambda-Block zurückgegeben wird. 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 leistungsstärker? Der Lambda-Block, den Sie dem Modifikator zur Verfügung stellen, wird während der Layout-Phase aufgerufen (genauer gesagt während des Platzierungsschritts der Layout-Phase). Das bedeutet, dass der Status firstVisibleItemScrollOffset nicht mehr während der Komposition gelesen wird. Da Compose verfolgt, wann der Status gelesen wird, bedeutet diese Änderung, dass Compose nur die Layout- und Zeichnungsphasen neu starten muss, wenn sich der value von firstVisibleItemScrollOffset ändert.
Natürlich ist es oft unbedingt erforderlich, Status 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 hierzu finden Sie unter derivedStateOf: Ein oder mehrere Statusobjekte in einen anderen
Statusumwandeln.
Neuzusammensetzungsschleife (zyklische Phasenabhängigkeit)
In diesem Leitfaden wurde bereits erwähnt, dass die Phasen von Compose immer in derselben Reihenfolge aufgerufen werden und dass es nicht möglich ist, innerhalb desselben Frames zurückzugehen. Das verhindert jedoch nicht, dass Apps in Kompositionsschleifen über verschiedene Frames hinweg geraten. Dazu ein Beispiel:
Box { var imageHeightPx by remember { mutableIntStateOf(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. Mit Modifier.onSizeChanged() wird die aufgelöste Größe des Bildes abgerufen und dann mit Modifier.padding() der Text nach unten verschoben.
Die unnatürliche Umwandlung von Px zurück in Dp deutet bereits darauf hin, dass es ein Problem mit dem Code gibt.
Das Problem in diesem Beispiel ist, dass der Code nicht innerhalb eines einzelnen Frames zum „endgültigen“ Layout gelangt. Der Code erfordert mehrere Frames, was unnötige Arbeit verursacht und dazu führt, dass die UI für den Nutzer auf dem Bildschirm hin und her springt.
Komposition des ersten Frames
Während der Kompositionsphase des ersten Frames ist imageHeightPx zunächst 0. Folglich wird der Text mit Modifier.padding(top = 0) versehen.
In der nachfolgenden Layout-Phase wird der Callback des Modifikators onSizeChanged aufgerufen, der imageHeightPx auf die tatsächliche Höhe des Bildes aktualisiert. Compose plant dann eine Neuzusammensetzung für den nächsten Frame. Während der aktuellen Zeichnungsphase wird der Text jedoch mit einem Padding von 0 gerendert, da der aktualisierte Wert von imageHeightPx noch nicht berücksichtigt wird.
Komposition des zweiten Frames
Compose initiiert den zweiten Frame, ausgelöst durch die Änderung des Werts von imageHeightPx. In der Kompositionsphase dieses Frames wird der Status innerhalb des Inhaltsblocks Box gelesen. Der Text wird jetzt mit einem Padding versehen, das genau der Höhe des Bildes entspricht. Während der Layout-Phase wird imageHeightPx noch einmal festgelegt. Es wird jedoch keine weitere Neuzusammensetzung geplant, da der Wert gleich bleibt.
Dieses Beispiel mag konstruiert wirken, aber achten Sie auf dieses allgemeine Muster:
Modifier.onSizeChanged(),onGloballyPositioned()oder andere Layout-Vorgänge- Status aktualisieren
- Diesen Status als Eingabe für einen Layout-Modifikator verwenden (
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 einer Column() implementiert werden. Möglicherweise haben Sie jedoch 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 Source of Truth für mehrere UI-Elemente zu haben, die in Bezug zueinander gemessen und platziert werden sollen. Wenn Sie ein geeignetes Layout-Grundelement verwenden oder ein benutzerdefiniertes Layout erstellen, dient das kleinste gemeinsame übergeordnete Element als Source of Truth, mit der die Beziehung zwischen mehreren Elementen koordiniert werden kann. Durch die Einführung eines dynamischen Status wird dieses Prinzip verletzt.
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Status und Jetpack Compose
- Listen und Raster
- Kotlin für Jetpack Compose