Lebenszyklus von zusammensetzbaren Funktionen

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

Überblick über den Lebenszyklus

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

Wenn Jetpack Compose Ihre zusammensetzbaren Funktionen zum ersten Mal ausführt, während der ersten Komposition, werden die zusammensetzbaren Funktionen erfasst, die Sie aufrufen, um Ihre Benutzeroberfläche in einer Komposition zu beschreiben. Wenn sich dann der Status Ihrer App ändert, plant Jetpack Compose eine Neuzusammensetzung. Bei der Neuzusammensetzung führt Jetpack Compose die zusammensetzbaren Funktionen noch einmal aus, die sich aufgrund von Statusänderungen möglicherweise geändert haben, und aktualisiert dann die Komposition, um alle Änderungen widerzuspiegeln.

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

Diagramm mit dem Lebenszyklus eines Composables
Abbildung 1 Lebenszyklus einer zusammensetzbaren Funktion in der Komposition. Sie tritt in die Komposition ein, wird 0 oder mehr Mal neu zusammengesetzt und verlässt die Komposition.

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

Wenn eine zusammensetzbare Funktion mehrmals aufgerufen wird, werden mehrere Instanzen in der Komposition platziert. Jeder Aufruf hat einen 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 eine zusammensetzbare Funktion mehrmals aufgerufen wird, werden mehrere Instanzen in der Komposition platziert. Ein Element mit einer anderen Farbe weist darauf hin, dass es sich um eine separate Instanz handelt.

Anatomie einer zusammensetzbaren Funktion in der Komposition

Die Instanz einer zusammensetzbaren Funktion in der Komposition wird durch ihre Aufrufstelle identifiziert. Der Compose-Compiler betrachtet jede Aufrufstelle als unterschiedlich. Wenn zusammensetzbare Funktionen von mehreren Aufrufstellen aufgerufen werden, werden mehrere Instanzen der zusammensetzbaren Funktion in der Komposition erstellt.

Wenn eine zusammensetzbare Funktion während einer Neuzusammensetzung andere zusammensetzbare Funktionen aufruft als bei der vorherigen Komposition, identifiziert Compose, welche zusammensetzbaren Funktionen aufgerufen wurden und welche nicht. Bei den zusammensetzbaren Funktionen, die in beiden Kompositionen aufgerufen wurden, vermeidet Compose die Neuzusammensetzung, wenn sich ihre Eingaben nicht geändert haben.

Das Beibehalten der Identität ist entscheidend, um Nebeneffekte mit der entsprechenden 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 verwendet, um ihn eindeutig zu identifizieren.

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 Neuzusammensetzung erfolgt. Dieselbe Farbe bedeutet, dass sie nicht neu zusammengesetzt wurde.

Obwohl LoginInput zuerst und dann als Zweites aufgerufen wurde, bleibt die LoginInput-Instanz bei Neuzusammensetzungen erhalten. Da LoginInput außerdem 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 Neuzusammensetzungen hinzufügen

Wenn eine zusammensetzbare Funktion mehrmals aufgerufen wird, wird sie auch mehrmals zur Komposition hinzugefügt. Wenn eine zusammensetzbare Funktion mehrmals von derselben Aufrufstelle aufgerufen wird, hat Compose keine Informationen, um jeden Aufruf dieser zusammensetzbaren Funktion eindeutig zu identifizieren. Daher wird zusätzlich zur Aufrufstelle auch die Ausführungsreihenfolge verwendet, um die Instanzen zu unterscheiden. Dieses Verhalten ist manchmal alles, was erforderlich ist, kann aber in einigen Fällen 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 obigen Beispiel verwendet Compose zusätzlich zur Aufrufstelle auch die Ausführungsreihenfolge, um die Instanz in der Komposition zu unterscheiden. Wenn der Liste ein neuer movie unten 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 die Eingabe movie 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. Zusammensetzbare Funktionen vom Typ MovieOverview in der Komposition können wiederverwendet werden. Dieselbe Farbe in MovieOverview bedeutet, dass die zusammensetzbare Funktion nicht neu zusammengesetzt wurde.

Wenn sich die Liste movies jedoch ändert, indem Elemente oben oder in der Mitte der Liste hinzugefügt, entfernt oder neu angeordnet werden, führt dies zu einer Neuzusammensetzung in allen MovieOverview-Aufrufen, deren Eingabeparameter in der Liste eine andere Position hat. Das ist besonders wichtig, wenn MovieOverview beispielsweise ein Filmbild mithilfe eines Nebeneffekts abruft. Wenn die Neuzusammensetzung erfolgt, während der Effekt ausgeführt wird, 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 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. Zusammensetzbare Funktionen vom Typ MovieOverview können nicht wiederverwendet werden und alle Nebeneffekte werden neu gestartet. Eine andere Farbe in MovieOverview bedeutet, dass die zusammensetzbare Funktion neu zusammengesetzt wurde.

Idealerweise sollte die Identität der MovieOverview-Instanz mit der Identität des movie verknüpft sein, der 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-Funktion mit einer anderen Filminstanz neu zusammenzusetzen. Compose bietet eine Möglichkeit, der Laufzeit mitzuteilen, welche Werte Sie verwenden möchten, um einen bestimmten Teil des Baums zu identifizieren: die key zusammensetzbare Funktion.

Wenn Sie einen Codeblock mit einem Aufruf der zusammensetzbaren Funktion „key“ umschließen und einen oder mehrere Werte übergeben, werden diese Werte kombiniert, um diese Instanz in der Komposition zu identifizieren. Der Wert für einen key muss nicht global eindeutig sein, sondern nur unter den Aufrufen von zusammensetzbaren Funktionen an der Aufrufstelle. In diesem Beispiel muss jeder movie einen key haben, der unter den movies eindeutig ist. Es ist in Ordnung, wenn er diesen key mit einer anderen zusammensetzbaren Funktion an einer anderen 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 zusammensetzbaren Funktionen vom Typ MovieOverview eindeutige Schlüssel haben, erkennt Compose, welche MovieOverview-Instanzen sich nicht geändert haben, und kann sie wiederverwenden. Ihre Nebeneffekte werden weiter ausgeführt.

Einige zusammensetzbare Funktionen bieten integrierte Unterstützung für die zusammensetzbare Funktion key. Beispielsweise kann in der items-DSL von LazyColumn ein benutzerdefinierter key angegeben werden.

@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 Neuzusammensetzung 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 kommt für das Überspringen infrage, es sei denn:

  • Die Funktion hat einen Rückgabetyp, der nicht Unit ist.
  • Die Funktion ist mit @NonRestartableComposable oder @NonSkippableComposable annotiert.
  • Ein erforderlicher Parameter hat einen nicht stabilen Typ.

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

Damit ein Typ als stabil gilt, muss er die folgenden Anforderungen erfüllen:

  • Das Ergebnis von equals für zwei Instanzen ist für dieselben beiden Instanzen immer gleich.
  • Wenn sich eine öffentliche Property des Typs ändert, wird die Komposition benachrichtigt.
  • Alle öffentlichen Property-Typen sind ebenfalls stabil.

Es gibt einige wichtige gängige Typen, die diese Anforderungen erfüllen 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 die Anforderungen für stabil erfüllen, da sie unveränderlich sind. Da sich unveränderliche Typen nie ändern, müssen sie die Komposition nicht über die Änderung benachrichtigen. Daher ist es viel einfacher, diese Anforderungen zu erfüllen.

Ein bemerkenswerter 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 Property 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 alle Werte seit dem vorherigen Aufruf unverändert sind.

Compose betrachtet einen Typ nur dann als stabil, wenn dies nachgewiesen werden kann. Eine Schnittstelle wird beispielsweise in der Regel als nicht stabil behandelt. Typen mit veränderlichen öffentlichen Properties, 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 @Stable Annotation.

// 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
}

Im obigen Code-Snippet könnte Compose UiState als nicht stabil betrachten, da es sich um eine Schnittstelle handelt. Durch Hinzufügen der Annotation @Stable teilen Sie Compose mit, dass dieser Typ stabil ist, sodass Compose intelligente Neuzusammensetzungen bevorzugen kann. Das bedeutet auch, dass Compose alle Implementierungen als stabil behandelt, wenn die Schnittstelle als Parametertyp verwendet wird.