Lebenszyklus von zusammensetzbaren Funktionen

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

Lebenszyklusübersicht

Wie in der Dokumentation zum Verwaltungsstatus erwähnt, beschreibt eine Komposition die Benutzeroberfläche Ihrer Anwendung und wird durch das Ausführen von zusammensetzbaren Funktionen erstellt. Eine Komposition ist eine Baumstruktur der zusammensetzbaren Funktionen, die Ihre Benutzeroberfläche beschreibt.

Wenn Jetpack Compose Ihre zusammensetzbaren Funktionen zum ersten Mal ausführt, werden sie während der ersten Zusammensetzung gespeichert. Wenn sich der Status der Anwendung ändert, plant Jetpack Compose eine Neuzusammensetzung. Bei der Neuzusammensetzung führt Jetpack Compose die Zusammensetzungen, die sich möglicherweise als Reaktion auf Statusänderungen geändert haben, noch einmal aus und aktualisiert dann die Zusammensetzung, um etwaige Änderungen widerzuspiegeln.

Eine Komposition kann nur aus einer ursprünglichen Komposition erstellt und durch Neuzusammensetzung aktualisiert werden. Die einzige Möglichkeit, eine Komposition zu ändern, ist die Neuzusammensetzung.

Diagramm, das den Lebenszyklus einer zusammensetzbaren Funktion zeigt

Abbildung 1: Lebenszyklus einer zusammensetzbaren Funktion in der Komposition. Sie tritt in die Komposition ein, wird null-mal oder öfter neu zusammengesetzt und verlässt die Komposition.

Die Neuzusammensetzung wird normalerweise durch eine Änderung an einem State<T>-Objekt ausgelöst. Die Funktion „Compose“ verfolgt diese und führt alle zusammensetzbaren Funktionen in der Komposition, die diese bestimmte State<T> lesen, sowie alle von ihr aufgerufenen zusammensetzbaren Funktionen aus, die nicht übersprungen werden können.

Wird eine zusammensetzbare Funktion mehrmals aufgerufen, werden in der Zusammensetzung mehrere Instanzen platziert. Jeder Aufruf hat in der Komposition einen eigenen Lebenszyklus.

@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 Wird eine zusammensetzbare Funktion mehrmals aufgerufen, werden in der Zusammensetzung mehrere Instanzen 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 über ihre Aufrufwebsite identifiziert. Der Compose-Compiler betrachtet jede Aufrufwebsite als eigenständig. Wenn Sie zusammensetzbare Funktionen von mehreren Aufrufwebsites aufrufen, werden mehrere Instanzen der zusammensetzbaren Funktion in der Funktion „Composition“ erstellt.

Wenn eine zusammensetzbare Funktion bei der Neuzusammensetzung andere zusammensetzbare Funktionen als während der vorherigen Erstellung aufruft, wird in der Funktion „Compose“ ermittelt, welche zusammensetzbaren Funktionen aufgerufen wurden oder nicht. Für die in beiden Kompositionen aufgerufenen zusammensetzbaren Funktionen vermeidet die Funktion „Neuzusammensetzung“, wenn sich ihre Eingaben nicht geändert haben.

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

Hier 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 Code-Snippet oben ruft LoginScreen die zusammensetzbare Funktion LoginError bedingt und immer die zusammensetzbare Funktion LoginInput auf. Jeder Aufruf hat eine eindeutige Aufrufwebsite und Quellposition, anhand derer der Compiler ihn eindeutig identifizieren kann.

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 Zusammensetzung, wenn sich der Status ändert und eine Neuzusammensetzung stattfindet. Gleiche Farbe bedeutet, dass es nicht neu zusammengesetzt wurde.

Obwohl LoginInput statt des ersten Aufrufs als zweiter aufgerufen wurde, wird die LoginInput-Instanz bei allen Neuzusammensetzungen beibehalten. Da LoginInput keine Parameter enthält, die sich bei der Neuzusammensetzung geändert haben, wird der Aufruf von LoginInput außerdem von der Funktion „Compose“ übersprungen.

Zusätzliche Informationen für intelligente Neuzusammensetzungen

Wenn Sie eine zusammensetzbare Funktion mehrmals aufrufen, wird sie auch mehrmals der Komposition hinzugefügt. Wenn eine zusammensetzbare Funktion mehrmals von derselben Aufrufwebsite aufgerufen wird, hat Compose keine Informationen, um jeden Aufruf dieser zusammensetzbaren Funktion eindeutig zu identifizieren. Daher wird die Ausführungsreihenfolge zusätzlich zur Aufrufwebsite verwendet, um die Instanzen voneinander zu unterscheiden. Dieses Verhalten ist manchmal allein, kann aber in manchen 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 Beispiel oben verwendet Compose die Ausführungsreihenfolge zusätzlich zur Aufrufwebsite, damit die Instanz in der Komposition unterschiedlich bleibt. Wird eine neue movie am unten der Liste hinzugefügt, kann Compose die Instanzen wiederverwenden, die bereits in der Zusammensetzung vorhanden sind, da sich ihr Speicherort in der Liste nicht geändert hat und daher die movie-Eingabe für diese Instanzen identisch ist.

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn ein neues Element am Ende der Liste hinzugefügt wird. Die anderen Elemente in der Liste haben ihre Position nicht geändert und werden nicht neu zusammengesetzt.

Abbildung 4: Darstellung von MoviesScreen in der Zusammensetzung, wenn am Ende der Liste ein neues Element hinzugefügt wird. MovieOverview- zusammensetzbare Funktionen in der Zusammensetzung können wiederverwendet werden. Gleiche Farbe in MovieOverview bedeutet, dass die zusammensetzbare Funktion nicht neu zusammengesetzt wurde.

Wenn sich die Liste movies jedoch ändert, indem Elemente an den Anfang oder in die Mitte der Liste aufgenommen oder Elemente entfernt oder neu angeordnet werden, erfolgt eine Neuzusammensetzung in allen MovieOverview-Aufrufen, deren Eingabeparameter sich in der Liste geändert hat. Das ist besonders wichtig, wenn MovieOverview beispielsweise ein Filmbild mit einem Nebeneffekt abruft. Wenn die Neuzusammensetzung stattfindet, während der Effekt ausgeführt wird, wird sie 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 ein neues Element am Anfang der Liste hinzugefügt wird. Alle anderen Listeneinträge ändern ihre Position und müssen neu zusammengesetzt werden.

Abbildung 5: Darstellung von MoviesScreen in der Zusammensetzung, wenn der Liste ein neues Element hinzugefügt wird. MovieOverview zusammensetzbare Funktionen 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 ist die Identität der MovieOverview-Instanz mit der Identität der movie verknüpft, die an sie übergeben wird. Wenn wir die Liste der Filme neu anordnen, würden wir idealerweise die Instanzen im Zusammensetzungsbaum ähnlich neu anordnen, anstatt jede zusammensetzbare Funktion MovieOverview mit einer anderen Filminstanz neu zusammenzusetzen. Mit Compose können Sie der Laufzeit mitteilen, welche Werte Sie verwenden möchten, um einen bestimmten Teil der Baumstruktur zu identifizieren: die zusammensetzbare Funktion key.

Durch das Umschließen eines Codeblocks in einen Aufruf der zusammensetzbaren Funktion mit einem oder mehreren übergebenen Werten werden diese Werte kombiniert, um diese Instanz in der Zusammensetzung zu identifizieren. Der Wert für key muss nicht global eindeutig sein, sondern nur unter den Aufrufen von zusammensetzbaren Funktionen auf der Aufrufseite eindeutig sein. In diesem Beispiel muss also jeder movie eine key haben, die unter den movies eindeutig ist. Es ist in Ordnung, wenn er diese key mit einer anderen zusammensetzbaren Funktion 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)
            }
        }
    }
}

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

Diagramm, das zeigt, wie der vorherige Code neu zusammengesetzt wird, wenn ein neues Element am Anfang der Liste hinzugefügt wird. Da die Listenelemente durch Schlüssel identifiziert werden, weiß das Tool, dass die Listenelemente nicht neu zusammengesetzt werden, auch wenn sich ihre Positionen geändert haben.

Abbildung 6: Darstellung von MoviesScreen in der Zusammensetzung, wenn der Liste ein neues Element hinzugefügt wird. Da die zusammensetzbaren MovieOverview-Objekte eindeutige Schlüssel haben, erkennt das Tool, welche MovieOverview-Instanzen nicht geändert wurden, und können diese wiederverwenden. Ihre Nebeneffekte werden weiterhin ausgeführt.

Einige zusammensetzbare Funktionen bieten integrierte Unterstützung für die zusammensetzbare Funktion key. Für LazyColumn kann beispielsweise ein benutzerdefinierter key in der items-DSL 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

Bei der Neuzusammensetzung kann die Ausführung einiger zulässiger zusammensetzbarer Funktionen komplett übersprungen werden, wenn sich ihre Eingaben im Vergleich zur vorherigen Zusammensetzung nicht geändert haben.

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

  • Die Funktion hat einen Nicht-Unit-Rückgabetyp
  • Die Funktion wird mit @NonRestartableComposable oder @NonSkippableComposable annotiert.
  • Ein erforderlicher Parameter hat einen instabilen Typ

Mit dem experimentellen Compiler-Modus Strong Skipping wird die letzte Anforderung gelockert.

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

  • Das Ergebnis von equals für zwei Instanzen ist für die beiden Instanzen endgültig dasselbe.
  • Wenn sich eine öffentliche Property dieses Typs ändert, wird die Komposition benachrichtigt.
  • Alle Typen öffentlicher Properties sind ebenfalls stabil.

Unter diesen Vertrag fallen einige wichtige gängige Typen, die vom Composer-Compiler als stabil behandelt werden, auch wenn sie mit der Annotation @Stable nicht explizit als stabil gekennzeichnet sind:

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

Alle diese Typen können dem Vertrag der stabilen Version folgen, da sie unveränderlich sind. Da sich unveränderliche Typen nie ändern, müssen sie die Zusammensetzung nie über die Änderung informieren. Daher ist es viel einfacher, diesem Vertrag zu folgen.

Ein wichtiger, stabiler, aber änderbarer Typ ist der Typ MutableState von „Compose“. Wenn ein Wert in einem MutableState enthalten ist, gilt das Statusobjekt insgesamt als stabil, da „Compose“ über jede Änderung am Attribut .value von State benachrichtigt wird.

Wenn alle Typen, die als Parameter an eine zusammensetzbare Funktion übergeben werden, stabil sind, werden die Parameterwerte basierend auf der zusammensetzbaren Position in der UI-Struktur auf Übereinstimmung verglichen. Die Neuzusammensetzung wird übersprungen, wenn alle Werte seit dem vorherigen Aufruf unverändert geblieben sind.

Bei „Compose“ gilt ein Typ nur dann als stabil, wenn er bewiesen werden kann. Beispielsweise wird eine Schnittstelle im Allgemeinen als nicht stabil behandelt. Ebenso sind Typen mit änderbaren öffentlichen Attributen, deren Implementierung unveränderlich sein könnte, nicht stabil.

Wenn Compose nicht ermitteln kann, ob ein Typ stabil ist, Sie ihn aber als stabil behandeln möchten, können Sie ihn mit der Annotation @Stable markieren.

// 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 im Code-Snippet UiState eine Schnittstelle ist, könnte Compose diesen Typ normalerweise als nicht stabil betrachten. Durch das Hinzufügen der Annotation @Stable teilen Sie der Funktion „Compose“ mit, dass dieser Typ stabil ist, sodass intelligente Neuzusammensetzungen bevorzugt werden. Das bedeutet auch, dass Compose alle Implementierungen als stabil behandelt, wenn die Schnittstelle als Parametertyp verwendet wird.