Lebenszyklus von zusammensetzbaren Funktionen

Auf dieser Seite erfahren Sie mehr über den Lebenszyklus einer Composable-Funktion und darüber, wie Compose entscheidet, ob eine Composable-Funktion neu zusammengesetzt werden muss.

Übersicht über den Lebenszyklus

Wie in der Dokumentation zum Verwalten des Status beschrieben, wird die Benutzeroberfläche Ihrer App durch eine Komposition beschrieben, die durch Ausführen von Composables erstellt wird. Eine Komposition ist eine Baumstruktur der Composables, die Ihre Benutzeroberfläche beschreiben.

Wenn Jetpack Compose Ihre Composables zum ersten Mal ausführt, wird während der ersten Komposition nachverfolgt, welche Composables Sie aufrufen, um Ihre Benutzeroberfläche in einer Komposition zu beschreiben. Wenn sich der Status Ihrer App ändert, plant Jetpack Compose eine Neuzusammensetzung. Bei der Neukomposition werden die Composables, die sich aufgrund von Statusänderungen geändert haben, von Jetpack Compose noch einmal ausgeführt. Anschließend wird die Komposition aktualisiert, um alle Änderungen zu berücksichtigen.

Eine Komposition kann nur durch eine anfängliche Komposition erstellt und durch eine Neukomposition aktualisiert werden. Die einzige Möglichkeit, eine Komposition zu ändern, ist die Neukomposition.

Diagramm, das den Lebenszyklus eines Composables zeigt
Abbildung 1. Lebenszyklus eines Composables in der Komposition. Sie wird in die Komposition eingefügt, 0-mal oder öfter neu komponiert und verlässt die Komposition.

Die Neukomposition wird in der Regel durch eine Änderung an einem State<T>-Objekt ausgelöst. Compose verfolgt diese und führt alle Composables in der Composition aus, die diesen bestimmten State<T> lesen, sowie alle Composables, die sie aufrufen und die nicht übersprungen werden können.

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

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

Diagramm zur hierarchischen Anordnung der Elemente im vorherigen Code-Snippet
Abbildung 2. Darstellung von MyComposable in der Komposition. Wenn ein Composable mehrmals aufgerufen wird, werden mehrere Instanzen in die Komposition eingefügt. Wenn ein Element eine andere Farbe hat, ist das ein Hinweis darauf, dass es sich um eine separate Instanz handelt.

Aufbau einer Composable-Funktion in Composition

Die Instanz eines Composables in der Komposition wird durch den Aufrufpunkt identifiziert. Der Compose-Compiler betrachtet jede Aufrufstelle als eigenständig. Wenn Sie Composables von mehreren Aufrufstellen aus aufrufen, werden mehrere Instanzen des Composables in der Komposition erstellt.

Wenn bei einer Neukomposition ein Composable andere Composables aufruft als bei der vorherigen Komposition, erkennt Compose, welche Composables aufgerufen wurden und welche nicht. Bei den Composables, die in beiden Kompositionen aufgerufen wurden, wird eine Neukomposition vermieden, wenn sich ihre Eingaben nicht geändert haben.

Die Beibehaltung der Identität ist entscheidend, um Nebenwirkungen mit ihrer zusammensetzbaren Funktion zu verknüpfen, damit sie erfolgreich abgeschlossen werden können, anstatt bei jeder Neuzusammensetzung neu zu starten.

Dazu ein 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 obigen Code-Snippet ruft LoginScreen die zusammensetzbare Funktion LoginError bedingt und die zusammensetzbare Funktion LoginInput immer auf. Jeder Aufruf hat eine eindeutige Aufrufstelle und Quellposition, die der Compiler zur eindeutigen Identifizierung verwendet.

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn das Flag „showError“ in „true“ geändert wird. Die zusammensetzbare Funktion „LoginError“ wird hinzugefügt, die anderen zusammensetzbaren Funktionen werden jedoch nicht neu zusammengesetzt.
Abbildung 3: Darstellung von LoginScreen in der Komposition, wenn sich der Status ändert und eine Neukomposition erfolgt. Wenn die Farbe gleich ist, wurde das Bild nicht neu zusammengesetzt.

Auch wenn LoginInput nun als Zweites aufgerufen wird, wird die LoginInput-Instanz bei Recompositionen beibehalten. Da LoginInput außerdem keine Parameter hat, die sich bei der Neukomposition geändert haben, wird der Aufruf von LoginInput von Compose übersprungen.

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

Wenn Sie ein Composable mehrmals aufrufen, wird es auch mehrmals in die Komposition aufgenommen. Wenn ein Composable mehrmals vom selben Aufrufort aus aufgerufen wird, hat Compose keine Informationen, um jeden Aufruf dieses Composables eindeutig zu identifizieren. Daher wird zusätzlich zum Aufrufort auch die Ausführungsreihenfolge verwendet, um die Instanzen zu unterscheiden. Manchmal ist dieses Verhalten genau das, was Sie möchten, in anderen Fällen kann es jedoch zu unerwünschten Ergebnissen 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 obigen Beispiel verwendet Compose die Ausführungsreihenfolge zusätzlich zum Aufrufort, um die Instanz in der Komposition zu unterscheiden. Wenn ein neues movie unten in der Liste hinzugefügt wird, kann Compose die bereits in der Komposition vorhandenen Instanzen wiederverwenden, da sich ihre Position in der Liste nicht geändert hat und der movie-Eingabewert für diese Instanzen daher derselbe 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 wurden nicht neu zusammengesetzt.
Abbildung 4: Darstellung von MoviesScreen in der Komposition, wenn ein neues Element unten in der Liste hinzugefügt wird. MovieOverview-Composables in der Composition können wiederverwendet werden. Wenn die Farbe in MovieOverview gleich bleibt, bedeutet das, dass die Composable nicht neu zusammengesetzt wurde.

Wenn sich die movies-Liste jedoch ändert, indem Elemente oben oder in der Mitte der Liste hinzugefügt, entfernt oder neu angeordnet werden, führt dies zu einer Neukomposition in allen MovieOverview-Aufrufen, deren Eingabeparameter die Position in der Liste geändert hat. Das ist besonders wichtig, wenn MovieOverview beispielsweise ein Filmbild mit einem Nebeneffekt abruft. Wenn während des Effekts eine Neuzusammensetzung erfolgt, wird der Effekt 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 oben in der Liste ein neues Element hinzugefügt wird. Jedes zweite Element in der Liste ändert seine Position und muss neu zusammengesetzt werden.
Abbildung 5. Darstellung von MoviesScreen in der Komposition, wenn der Liste ein neues Element hinzugefügt wird. MovieOverview-Composables können nicht wiederverwendet werden und alle Nebeneffekte werden neu gestartet. Eine andere Farbe in MovieOverview bedeutet, dass die Composable neu zusammengesetzt wurde.

Idealerweise sollte die Identität der MovieOverview-Instanz mit der Identität der movie verknüpft sein, die an sie übergeben wird. Wenn wir die Liste der Filme neu anordnen, sollten wir idealerweise auch die Instanzen im Kompositionsbaum neu anordnen, anstatt jede MovieOverview-Composable mit einer anderen Filminstanz neu zu komponieren. Mit Compose können Sie der Laufzeit mitteilen, welche Werte Sie zur Identifizierung eines bestimmten Teils des Baums verwenden möchten: das Composable key.

Wenn Sie einen Codeblock mit einem Aufruf des Schlüssel-Composables mit einem oder mehreren übergebenen Werten umschließen, werden diese Werte kombiniert, um diese Instanz in der Komposition zu identifizieren. Der Wert für ein key muss nicht global eindeutig sein, sondern nur unter den Aufrufen von Composables am Aufrufort. In diesem Beispiel muss also jede movie ein key haben, das unter den movies eindeutig ist. Es ist in Ordnung, wenn es dieses key mit einem anderen Composable an anderer Stelle in der App teilt.

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

Auch wenn sich die Elemente in der Liste ändern, erkennt Compose einzelne Aufrufe von MovieOverview und kann sie wiederverwenden.

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn oben in der Liste ein neues Element hinzugefügt wird. Da die Listenelemente durch Schlüssel identifiziert werden, weiß Compose, dass sie nicht neu zusammengesetzt werden müssen, obwohl 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-Composable-Funktionen eindeutige Schlüssel haben, erkennt Compose, welche MovieOverview-Instanzen sich nicht geändert haben, und kann sie wiederverwenden. Ihre Nebeneffekte werden weiterhin ausgeführt.

Einige Composables bieten integrierte Unterstützung für das key-Composable. Beispiel: LazyColumn akzeptiert die Angabe eines 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

Während der Neukomposition kann die Ausführung einiger infrage kommender zusammensetzbarer Funktionen vollständig übersprungen werden, wenn sich ihre Eingaben seit der vorherigen Komposition nicht geändert haben.

Eine zusammensetzbare 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 nicht stabilen Typ.

Es gibt einen Compilermodus, Strong Skipping, der die letzte Anforderung lockert.

Damit ein Typ als stabil gilt, muss er den folgenden Vertrag erfüllen:

  • Das Ergebnis von equals für zwei Instanzen ist immer dasselbe für dieselben zwei Instanzen.
  • Wenn sich eine öffentliche Eigenschaft des Typs ändert, wird „Composition“ benachrichtigt.
  • Alle öffentlichen Attributtypen sind ebenfalls stabil.

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

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

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

Ein wichtiger Typ, der stabil, aber veränderlich ist, ist der MutableState-Typ von Compose. Wenn ein Wert in einem MutableState gespeichert ist, gilt das Statusobjekt insgesamt als stabil, da Compose über alle Änderungen an der .value-Eigenschaft von State benachrichtigt wird.

Wenn alle als Parameter an eine zusammensetzbare Funktion übergebenen Typen stabil sind, werden die Parameterwerte anhand der Position der zusammensetzbaren Funktion 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. Typen mit veränderlichen öffentlichen Eigenschaften, deren Implementierung unveränderlich sein könnte, sind ebenfalls nicht stabil.

Wenn Compose nicht ableiten kann, dass ein Typ stabil ist, Sie aber erzwingen möchten, dass Compose ihn als stabil behandelt, kennzeichnen Sie ihn mit der Annotation @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 oben stehenden Code-Snippet eine Schnittstelle ist, könnte Compose diesen Typ normalerweise als nicht stabil betrachten. Durch Hinzufügen der Annotation @Stable teilen Sie Compose mit, dass dieser Typ stabil ist. Dadurch kann Compose Smart Recompositions bevorzugen. Das bedeutet auch, dass Compose alle Implementierungen als stabil behandelt, wenn das Interface als Parametertyp verwendet wird.