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.
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") } }
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.
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.
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) /* ... */ } }
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.
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.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé.
- États et Jetpack Compose
- Effets secondaires dans Compose
- Enregistrer l'état de l'UI dans Compose