Cycle de vie des composables

Sur cette page, vous découvrirez le cycle de vie d'un composable et la manière dont Compose détermine si un composable doit être recomposé.

Présentation du cycle de vie

Comme indiqué dans la documentation Gérer l'état, une composition décrit l'interface utilisateur (UI) de votre application et est générée par l'exécution des composables. Une composition est une arborescence des composables décrivant votre interface utilisateur.

Lorsque Jetpack Compose exécute vos composables pour la première fois lors de la composition initiale, il suit les composables que vous appelez pour décrire votre UI dans une composition. Ensuite, lorsque l'état de votre application change, Jetpack Compose planifie une recomposition. La recomposition désigne le moment où Jetpack Compose réexécute les composables qui ont pu changer en réponse à des changements d'état, puis met à jour la composition pour refléter ces changements.

Une composition ne peut être produite que par une composition initiale et mise à jour par une recomposition. Le seul moyen de modifier une composition est la recomposition.

Diagramme illustrant le cycle de vie d'un composable

Figure 1. Cycle de vie d'un composable dans la composition. Il entre dans la composition, est recomposé zéro fois ou plus et sort de la composition.

La recomposition est généralement déclenchée par la modification d'un objet State<T>. Compose suit ces éléments et exécute tous les composables de la composition qui lisent cet objet State<T>, ainsi que tous les composables qu'ils appellent et qui ne peuvent pas être ignorés.

Si un composable est appelé plusieurs fois, plusieurs instances sont placées dans la composition. Chaque appel a son propre cycle de vie dans la composition.

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

Diagramme illustrant l&#39;organisation hiérarchique des éléments de l&#39;extrait de code ci-dessus

Figure 2. Représentation de MyComposable dans la composition. Si un composable est appelé plusieurs fois, plusieurs instances sont placées dans la composition. Un élément de couleur différente indique qu'il s'agit d'une instance distincte.

Anatomie d'un composable dans une composition

L'instance d'un composable dans une composition est identifiée par son site d'appel. Le compilateur Compose considère chaque site d'appel comme distinct. L'appel de composables à partir de plusieurs sites d'appel crée plusieurs instances du composable dans la composition.

Si lors d'une recomposition un composable appelle des composables différents par rapport à la composition précédente, Compose identifiera les composables appelés ou non. Pour les composables appelés dans les deux compositions, Compose évitera de les recomposer si leurs entrées n'ont pas changé.

Il est essentiel de préserver l'identité pour associer les effets secondaires à leur composable, afin qu'ils puissent s'exécuter correctement au lieu de redémarrer pour chaque recomposition.

Prenons l'exemple suivant :

@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() { /* ... */ }

Dans l'extrait de code ci-dessus, LoginScreen appelle le composable LoginError de manière conditionnelle et appelle toujours le composable LoginInput. Chaque appel dispose d'un site d'appel et d'une position source uniques que le compilateur utilisera pour l'identifier de manière unique.

Diagramme illustrant la recomposition du code précédent si l&#39;indicateur showError est défini sur &quot;true&quot; (vrai). Le composable LoginError est ajouté, mais les autres composables ne sont pas recomposés.

Figure 3. Représentation de LoginScreen dans la composition lorsque l'état change et qu'une recomposition se produit. Si la couleur ne change pas, alors il n'y a pas eu de recomposition.

Bien que LoginInput ne soit plus appelé en premier, mais en second, l'instance LoginInput sera préservée lors des recompositions. De plus, comme LoginInput ne comporte aucun paramètre modifié lors de la recomposition, Compose ignorera l'appel à LoginInput.

Ajouter des informations pour faciliter les recompositions intelligentes

Un composable appelé plusieurs fois est également ajouté plusieurs fois à la composition. Lorsque vous appelez un composable plusieurs fois à partir du même site d'appel, Compose ne dispose d'aucune information pour identifier chaque appel à ce composable. L'ordre d'exécution est donc utilisé en plus du site d'appel afin de conserver les instances séparées. Ce comportement est parfois suffisant, mais dans certains cas, il peut provoquer un comportement indésirable.

@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)
        }
    }
}

Dans l'exemple ci-dessus, Compose utilise l'ordre d'exécution en plus du site d'appel pour que l'instance reste distincte dans la composition. Si un nouveau movie est ajouté au bas de la liste, Compose peut réutiliser les instances déjà présentes dans la composition, car leur emplacement dans la liste n'a pas changé. Par conséquent, l'entrée movie est identique pour ces instances.

Diagramme illustrant comment le code précédent est recomposé si un nouvel élément est ajouté en bas de la liste. Les autres éléments de la liste n&#39;ont pas changé de position et ne sont pas recomposés.

Figure 4. Représentation de MoviesScreen dans la composition lorsqu'un nouvel élément est ajouté en bas de la liste. Les composables MovieOverview dans la composition peuvent être réutilisés. Si la couleur de MovieOverview ne change pas, alors il n'y a pas eu de recomposition.

Toutefois, si la liste movies change en ajoutant des éléments en haut ou au milieu de la liste, en en supprimant ou en les réorganisant, cela entraînera une recomposition dans tous les appels MovieOverview dont le paramètre d'entrée a changé de position dans la liste. Cela est extrêmement important si, par exemple, MovieOverview récupère une image "movie" en utilisant un effet secondaire. Si la recomposition a lieu alors que l'effet est en cours, elle est annulée et recommence.

@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)

        /* ... */
    }
}

Diagramme illustrant comment le code précédent est recomposé si un nouvel élément est ajouté en haut de la liste. Tous les autres éléments de la liste changent de position et doivent être recomposés.

Figure 5. Représentation de MoviesScreen dans la composition lorsqu'un nouvel élément est ajouté à la liste. Les composables MovieOverview ne peuvent pas être réutilisés, et tous les effets secondaires redémarrent. Une couleur différente dans MovieOverview signifie que le composable a été recomposé.

Idéalement, l'identité de l'instance MovieOverview doit être liée à l'identité de movie qui lui est transmise. Si vous réorganisez la liste des films, vous devrez également réorganiser les instances dans l'arborescence de composition plutôt que de recomposer chaque composable MovieOverview avec une instance "movie" différente. Compose vous permet d'indiquer à l'environnement d'exécution les valeurs que vous souhaitez utiliser pour identifier une partie donnée de l'arborescence : le composable key.

En encapsulant un bloc de code avec un appel au composable "key" avec une ou plusieurs valeurs transmises, ces valeurs seront combinées pour identifier cette instance dans la composition. La valeur d'une key n'a pas besoin d'être entièrement unique. Elle doit être unique parmi les invocations de composables sur le site d'appel. Ainsi, dans cet exemple, chaque movie doit avoir une key unique parmi les movies et ce n'est pas un problème si cette key est partagée avec un autre composable ailleurs dans l'application.

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

Avec ce qui a été expliqué précédemment, même si les éléments de la liste changent, Compose reconnaît les appels individuels à MovieOverview et peut les réutiliser.

Diagramme illustrant comment le code précédent est recomposé si un nouvel élément est ajouté en haut de la liste. Étant donné que les éléments de la liste sont identifiés par des &quot;keys&quot;, Compose sait qu&#39;il ne faut pas les recomposer, même si leur position a changé.

Figure 6. Représentation de MoviesScreen dans la composition lorsqu'un nouvel élément est ajouté à la liste. Comme les composables MovieOverview possèdent des "keys" uniques, Compose peut identifier quelles instances de MovieOverview non pas été modifiées et les réutiliser, leurs effets secondaires continueront de s'exécuter.

Certains composables sont compatibles avec le composable key. Par exemple, LazyColumn accepte la spécification d'une key personnalisée dans le DSL items.

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

Ignorer si les entrées n'ont pas changé

Lors de la recomposition, certaines fonctions composables éligibles peuvent être entièrement ignorées si leurs entrées n'ont pas changé par rapport à la composition précédente.

Une fonction composable peut être ignorée sauf si:

  • La fonction possède un type renvoyé autre que Unit
  • La fonction est annotée avec @NonRestartableComposable ou @NonSkippableComposable.
  • Un paramètre obligatoire est de type non stable

Il existe un mode de compilation expérimental, Strong Skipping (Saut fort), qui assouplit la dernière exigence.

Pour qu'un type soit considéré comme stable, il doit respecter le contrat suivant:

  • Le résultat de equals pour deux instances sera toujours identique pour les deux mêmes instances
  • Si une propriété publique de ce type change, la composition en sera informée
  • Tous les types de propriétés publiques sont également stables

Certains types courants importants relatifs à ce contrat sont considérés comme stables par le compilateur de composition, même s'ils ne sont pas explicitement marqués comme stables à l'aide de l'annotation @Stable:

  • Tous les types de valeurs primitives : Boolean, Int, Long, Float, Char, etc.
  • Strings
  • Tous les types de fonctions (lambdas)

Tous ces types peuvent suivre le contrat de stabilité, car ils sont immuables. Étant donné que les types immuables ne changent jamais, ils n'ont jamais à informer la composition de la modification. Il est donc beaucoup plus facile de respecter ce contrat.

Le type MutableState de Compose est stable, mais est modifiable. Si une valeur est stockée dans un MutableState, l'objet d'état dans son ensemble est considéré comme stable, car Compose sera informé de toute modification de la propriété .value de State.

Lorsque tous les types transmis en tant que paramètres à un composable sont stables, les valeurs des paramètres sont comparées pour déterminer l'égalité en fonction de la position du composable dans l'arborescence d'UI. La recomposition est ignorée si toutes les valeurs restent inchangées depuis l'appel précédent.

Compose ne considère un type comme stable que s'il peut le prouver. Par exemple, une interface est généralement considérée comme non stable, de même que les types de propriétés publiques modifiables dont l'implémentation pourrait être immuable.

Si Compose ne parvient pas à déduire qu'un type est stable, mais que vous souhaitez forcer Compose à le considérer comme tel, marquez-le avec l'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
}

Dans l'extrait de code ci-dessus, étant donné que UiState est une interface, Compose peut généralement considérer que ce type n'est pas stable. En ajoutant l'annotation @Stable, vous indiquez à Compose que ce type est stable, ce qui permet à Compose de favoriser les recompositions intelligentes. Cela signifie également que Compose considérera toutes ses implémentations comme stables si l'interface est utilisée comme type de paramètre.