Lebenszyklus von zusammensetzbaren Funktionen

Auf dieser Seite erfahren Sie mehr über den Lebenszyklus eines Composeables und wie in Compose entschieden wird, ob ein Composeable neu zusammengesetzt werden muss.

Lebenszyklus – Übersicht

Wie in der Dokumentation zum Verwalten des Zustands erwähnt, beschreibt eine Komposition die Benutzeroberfläche Ihrer App und wird durch Ausführen von Composeables erstellt. Eine Komposition ist eine Baumstruktur der Composeables, die Ihre Benutzeroberfläche beschreiben.

Wenn Jetpack Compose Ihre Composeables zum ersten Mal ausführt, wird während der ersten Zusammensetzung ein Überblick über die Composeables erstellt, die Sie aufrufen, um Ihre UI in einer Zusammensetzung zu beschreiben. Wenn sich der Status Ihrer App ändert, plant Jetpack Compose eine Neuzusammenstellung. Bei der Neuzusammensetzung führt Jetpack Compose die Composables, die sich möglicherweise aufgrund von Statusänderungen geändert haben, noch einmal aus und aktualisiert dann die Zusammensetzung entsprechend.

Eine Komposition kann nur durch eine anfängliche Komposition erstellt und durch eine Neukomposition aktualisiert werden. Eine Komposition kann nur durch Neuzusammenstellung geändert werden.

Diagramm, das den Lebenszyklus eines Composeables zeigt

Abbildung 1: Lebenszyklus eines Composeables in der Komposition. Es wird in die Komposition eingefügt, null oder mehrmals neu komponiert und verlässt die Komposition.

Die Neuzusammensetzung wird in der Regel durch eine Änderung an einem State<T>-Objekt ausgelöst. Compose überwacht diese und führt alle Composables in der Komposition aus, die diese bestimmte State<T> lesen, sowie alle Composables, die von ihnen aufgerufen werden und nicht übersprungen werden können.

Wenn ein Composeable mehrmals aufgerufen wird, werden mehrere Instanzen in die Komposition eingefügt. Jeder Aufruf hat seinen eigenen Lebenszyklus in der Komposition.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

Diagramm, das die hierarchische Anordnung der Elemente im vorherigen Code-Snippet zeigt

Abbildung 2: Darstellung von MyComposable in der Komposition. Wenn ein Composeable mehrmals aufgerufen wird, werden mehrere Instanzen in die Komposition eingefügt. Ein Element mit einer anderen Farbe weist auf eine separate Instanz hin.

Aufbau eines Composeables in der Funktion „Composition“

Die Instanz eines Composeables in der Komposition wird durch ihren Aufrufort identifiziert. Der Compose-Compiler betrachtet jede Aufrufstelle als eindeutig. Wenn Sie Composeables von mehreren Aufrufstellen aus aufrufen, werden mehrere Instanzen des Composeables in der Zusammensetzung erstellt.

Wenn bei einer Neuzusammensetzung ein Compose-Element andere Compose-Elemente aufruft als bei der vorherigen Zusammensetzung, ermittelt Compose, welche Compose-Elemente aufgerufen wurden oder nicht. Bei den Compose-Elementen, die in beiden Zusammensetzungen aufgerufen wurden, vermeidet Compose eine Neuzusammensetzung, wenn sich ihre Eingaben nicht geändert haben.

Die Identität muss beibehalten werden, damit Nebeneffekte mit ihren komponierbaren Elementen verknüpft werden können, damit sie erfolgreich abgeschlossen werden können, anstatt bei jeder Neuzusammensetzung neu gestartet zu werden.

Betrachten Sie das folgende Beispiel:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

Im Code-Snippet oben ruft LoginScreen die LoginError-Komposition bedingt und die LoginInput-Komposition immer auf. Jeder Aufruf hat eine eindeutige Aufrufstelle und Quellposition, anhand derer der Compiler ihn eindeutig identifizieren kann.

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn das Flag „showError“ auf „true“ gesetzt wird. Das „LoginError“-Komposit wird hinzugefügt, die anderen Kompositionen werden jedoch nicht neu zusammengesetzt.

Abbildung 3: Darstellung von LoginScreen in der Komposition, wenn sich der Status ändert und eine Neuzusammensetzung erfolgt. Wenn die Farbe gleich ist, wurde das Bild nicht neu zusammengesetzt.

Auch wenn LoginInput zuerst und dann als zweites Element aufgerufen wurde, bleibt die LoginInput-Instanz bei allen Neuzusammensetzungen erhalten. Da LoginInput keine Parameter hat, die sich bei der Neuzusammensetzung geändert haben, wird der Aufruf von LoginInput von Compose übersprungen.

Zusätzliche Informationen für intelligente Zusammenstellungen hinzufügen

Wenn Sie ein Composeable mehrmals aufrufen, wird es der Komposition ebenfalls mehrmals hinzugefügt. Wenn ein Composeable mehrmals von derselben Aufrufstelle aufgerufen wird, hat Compose keine Informationen, um jeden Aufruf dieses Composeables eindeutig zu identifizieren. Daher wird zusätzlich zur Aufrufstelle die Ausführungsreihenfolge verwendet, um die Instanzen voneinander zu unterscheiden. Manchmal ist dieses Verhalten alles, was erforderlich ist. In einigen Fällen kann es jedoch zu unerwünschtem Verhalten führen.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

Im Beispiel oben verwendet Compose zusätzlich zur Aufruf-Website die Ausführungsreihenfolge, um die Instanz in der Komposition eindeutig zu identifizieren. Wenn eine neue movie an das Ende der Liste hinzugefügt wird, können die Instanzen, die sich bereits in der Komposition befinden, wiederverwendet werden, da sich ihr Speicherort in der Liste nicht geändert hat und die movie-Eingabe für diese Instanzen daher gleich ist.

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn der Liste unten ein neues Element hinzugefügt wird. Die Position der anderen Elemente in der Liste hat sich nicht geändert und sie werden nicht neu zusammengesetzt.

Abbildung 4: Darstellung von MoviesScreen in der Komposition, wenn der Liste unten ein neues Element hinzugefügt wird. MovieOverview-Kompositionen in der Komposition können wiederverwendet werden. Wenn die Farbe in MovieOverview gleich ist, wurde das Composeable nicht neu zusammengesetzt.

Wenn sich die movies-Liste jedoch ändert, indem Elemente oben oder in der Mitte hinzugefügt, entfernt oder neu angeordnet werden, wird bei allen MovieOverview-Aufrufen, deren Eingabeparameter sich in der Liste verschoben hat, eine Neuzusammensetzung durchgeführt. Das ist äußerst wichtig, wenn MovieOverview beispielsweise ein Filmbild mithilfe eines Nebeneffekts abruft. Wenn die Neuzusammensetzung während der Ausführung des Effekts erfolgt, wird er abgebrochen und neu gestartet.

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn der Liste oben ein neues Element hinzugefügt wird. Alle anderen Elemente in der Liste ändern ihre Position und müssen neu zusammengesetzt werden.

Abbildung 5: Darstellung von MoviesScreen in der Komposition, wenn der Liste ein neues Element hinzugefügt wird. MovieOverview-Kompositionen können nicht wiederverwendet werden und alle Nebenwirkungen werden neu gestartet. Eine andere Farbe in MovieOverview bedeutet, dass das Composed-Element neu zusammengesetzt wurde.

Idealerweise sollten wir die Identität der MovieOverview-Instanz als mit der Identität der übergebenen movie verknüpft betrachten. Wenn wir die Liste der Filme neu anordnen, sollten wir idealerweise auch die Instanzen im Kompositionbaum neu anordnen, anstatt jede MovieOverview-Komposition mit einer anderen Filminstanz neu zusammenzustellen. Mit Compose können Sie der Laufzeit mitteilen, welche Werte Sie zum Identifizieren eines bestimmten Teils des Baums verwenden möchten: das key-komposit.

Wenn Sie einen Codeblock in einen Aufruf des Schlüssels einschließen, der mit einem oder mehreren übergebenen Werten zusammengesetzt werden kann, werden diese Werte kombiniert, um diese Instanz in der Komposition zu identifizieren. Der Wert für eine key muss nicht global eindeutig sein, sondern nur unter den Aufrufen von Composeables an der Aufrufstelle. In diesem Beispiel muss also jedes movie ein key haben, das unter den movies eindeutig ist. Es ist in Ordnung, wenn dieses key mit einem anderen Composeable an anderer Stelle in der App geteilt wird.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

So werden auch dann, wenn sich die Elemente in der Liste ändern, einzelne Aufrufe von MovieOverview in Compose erkannt und können wiederverwendet werden.

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn der Liste oben ein neues Element hinzugefügt wird. Da die Listenelemente anhand von Schlüsseln identifiziert werden, weiß Compose, dass sie nicht neu zusammengesetzt werden müssen, auch wenn sich ihre Positionen geändert haben.

Abbildung 6 Darstellung von MoviesScreen in der Komposition, wenn der Liste ein neues Element hinzugefügt wird. Da die MovieOverview-Kompositionen eindeutige Schlüssel haben, erkennt Compose, welche MovieOverview-Instanzen sich nicht geändert haben, und kann sie wiederverwenden. Die Nebeneffekte werden weiterhin ausgeführt.

Einige Composeables bieten integrierte Unterstützung für das key-Composeable. Beispiel: LazyColumn akzeptiert die Angabe einer benutzerdefinierten key in der items-DSL.

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

Überspringen, wenn sich die Eingaben nicht geändert haben

Bei der Neuzusammensetzung kann die Ausführung einiger geeigneter zusammensetzbarer Funktionen vollständig übersprungen werden, wenn sich ihre Eingaben seit der vorherigen Zusammensetzung nicht geändert haben.

Eine komponierbare Funktion kann übersprungen werden, es sei denn:

  • Die Funktion hat einen anderen Rückgabetyp als Unit.
  • Die Funktion ist mit @NonRestartableComposable oder @NonSkippableComposable annotiert.
  • Ein erforderlicher Parameter hat einen instabilen Typ

Es gibt einen experimentellen Compilermodus, Strong Skipping, bei dem die letzte Anforderung gelockert wird.

Damit ein Typ als stabil gilt, muss er dem folgenden Vertrag entsprechen:

  • Das Ergebnis von equals für zwei Instanzen ist immer dasselbe.
  • Wenn sich eine öffentliche Eigenschaft des Typs ändert, wird Composition benachrichtigt.
  • Auch alle öffentlichen Unterkünfte sind stabil.

Es gibt einige wichtige gängige Typen, die unter diesen Vertrag fallen und vom Compose-Compiler als stabil behandelt werden, auch wenn sie nicht explizit mit der Anmerkung @Stable als stabil gekennzeichnet sind:

  • Alle primitiven Werttypen: Boolean, Int, Long, Float, Char usw.
  • Strings
  • Alle Funktionstypen (Lambdas)

Alle diese Typen können dem Vertrag von „stabil“ folgen, da sie unveränderlich sind. Da sich unveränderliche Typen nie ändern, müssen sie die Zusammensetzung der Änderung nie benachrichtigen. Daher ist es viel einfacher, diesen Vertrag einzuhalten.

Ein bemerkenswerter Typ, der stabil, aber veränderbar ist, ist der MutableState-Typ von Compose. Wenn ein Wert in einer MutableState gehalten wird, gilt das Statusobjekt insgesamt als stabil, da Compose über alle Änderungen an der .value-Eigenschaft von State informiert wird.

Wenn alle Typen, die einem Composeable als Parameter übergeben werden, stabil sind, werden die Parameterwerte basierend auf der Position des Composeable im UI-Baum auf Gleichheit verglichen. Die Neuzusammensetzung wird übersprungen, wenn sich alle Werte seit dem vorherigen Aufruf nicht geändert haben.

Compose betrachtet einen Typ nur dann als stabil, wenn dies nachgewiesen werden kann. Beispielsweise wird eine Schnittstelle im Allgemeinen als nicht stabil behandelt. Auch Typen mit veränderlichen öffentlichen Eigenschaften, deren Implementierung unveränderlich sein könnte, sind nicht stabil.

Wenn Compose nicht in der Lage ist, zu erkennen, dass ein Typ stabil ist, Sie ihn aber zwingen möchten, ihn als stabil zu behandeln, kennzeichnen Sie ihn mit der Anmerkung @Stable.

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

Da UiState im obigen Code-Snippet eine Schnittstelle ist, könnte Compose diesen Typ normalerweise als nicht stabil betrachten. Wenn Sie die Anmerkung @Stable hinzufügen, teilen Sie Compose mit, dass dieser Typ stabil ist. Compose kann dann intelligente Neuzusammensetzungen bevorzugen. Das bedeutet auch, dass Compose alle seine Implementierungen als stabil behandelt, wenn die Schnittstelle als Parametertyp verwendet wird.