Comme la plupart des autres kits d'outils de l'interface utilisateur, Compose affiche un frame à l'aide de plusieurs phases distinctes. Le système de vues Android comporte trois phases principales : la mesure, la mise en page et le dessin. Compose est très similaire, mais inclut une phase supplémentaire clé appelée composition au début.
Celle-ci est décrite dans nos documents Compose, y compris dans les articles Raisonnement dans Compose et États et Jetpack Compose.
Les trois phases d'un frame
Compose compte trois phases principales :
- Composition : quels éléments d'interface utilisateur afficher. Compose exécute des fonctions modulables et crée une description de votre UI.
- Mise en page : où positionner les éléments d'interface utilisateur. Cette phase comprend deux étapes : la mesure et le positionnement. Les éléments de mise en page se mesurent et se placent eux-mêmes ainsi que tous les éléments enfants en coordonnées 2D, pour chaque nœud de l'arborescence de mise en page.
- Dessin : comment effectuer le rendu de l'interface utilisateur. Les éléments de l'interface utilisateur apparaissent dans un canevas, habituellement l'écran d'un d'appareil.
![Image des trois phases au cours desquelles Compose transforme les données en UI (dans l'ordre : données, composition, mise en page, dessin, UI).](https://developer.android.com/static/develop/ui/compose/images/compose-phases.png?authuser=5&hl=fr)
L'ordre de ces phases est généralement le même, ce qui permet aux données de circuler dans un sens (de la composition à la mise en page, en passant par le dessin) afin de générer un frame (également appelé flux de données unidirectionnel).
BoxWithConstraints
, LazyColumn
et LazyRow
constituent des exceptions notables, où la composition des enfants dépend de la phase de mise en page du parent.
Il serait logique de penser que ces trois phases se produisent pratiquement pour chaque frame, mais pour des raisons de performances, Compose évite de répéter une tâche qui calculerait les mêmes résultats à partir des mêmes entrées dans toutes ces phases. Compose ignore l'exécution d'une fonction modulable si un ancien résultat peut être réutilisé, et l'interface utilisateur de Compose ne recommence pas la mise en page ou le dessin de toute l'arborescence s'il n'y a pas lieu de le faire. Il ne fournit que les efforts minimums nécessaires pour mettre à jour l'interface utilisateur. Cette optimisation est possible, car Compose suit les lectures d'état au cours des différentes phases.
Comprendre les phases
Cette section décrit comment les trois phases de Compose sont exécutées pour les composables plus en détail.
Composition
Dans la phase de composition, l'environnement d'exécution Compose exécute des fonctions composables et génère une arborescence qui représente votre UI. Cette arborescence d'UI comprend les nœuds de mise en page qui contiennent toutes les informations nécessaires pour les phases suivantes, comme dans la vidéo suivante:
Figure 2. Arborescence représentant votre UI créée dans la composition à chaque phase.
Une sous-section du code et de l'arborescence de l'interface utilisateur se présente comme suit:
![Un extrait de code avec cinq composables et l'arborescence d'UI qui en résulte, avec des nœuds enfants reliés à leurs nœuds parents.](https://developer.android.com/static/develop/ui/compose/images/code-subsection.png?authuser=5&hl=fr)
Dans ces exemples, chaque fonction composable du code correspond à une seule mise en page. dans l'arborescence de l'UI. Dans des exemples plus complexes, les composables peuvent contenir une logique et et créer une arborescence différente selon différents états.
Mise en page
Lors de la phase de mise en page, Compose utilise l'arborescence d'UI produite lors de la phase de composition. comme entrée. L'ensemble de nœuds de mise en page contient toutes les informations nécessaires pour décider de la taille et de l'emplacement de chaque nœud dans un espace bidimensionnel.
Figure 4. Mesure et placement de chaque nœud de mise en page dans l'arborescence de l'interface utilisateur pendant la phase de mise en page.
Lors de la phase de mise en page, l'arborescence est balayée selon les trois étapes suivantes : algorithme:
- Mesurer les enfants: un nœud mesure ses enfants s'il en existe.
- Choisir sa propre taille: sur la base de ces mesures, un nœud choisit lui-même la taille de l'image.
- Placer les enfants: chaque nœud enfant est placé par rapport au propre la position de votre annonce.
À la fin de cette phase, chaque nœud de mise en page dispose des éléments suivants:
- Des attributs width et height donnés.
- Coordonnée x/y à l'endroit où elle doit être tracée
Rappelez l'arborescence de l'interface utilisateur de la section précédente:
Pour cet arbre, l'algorithme fonctionne comme suit:
Row
mesure ses enfants,Image
etColumn
.Image
est mesuré. Comme il n'a pas d'enfants, il décide et renvoie cette valeur àRow
.- La
Column
est ensuite mesurée. Il mesure ses propres enfants (deux élémentsText
composables) d'abord. - La première
Text
est mesurée. Comme il n'a pas d'enfants, il décide et renvoie sa taille àColumn
.- Le deuxième
Text
est mesuré. Comme il n'a pas d'enfants, il décide et la transmet auColumn
.
- Le deuxième
Column
utilise les mesures enfants pour décider de sa propre taille. Elle utilise le la largeur maximale de l'élément enfant et la somme de la hauteur de ses enfants.Column
place ses enfants par rapport à lui-même, ce qui les place en dessous. les uns les autres verticalement.Row
utilise les mesures enfants pour décider de sa propre taille. Elle utilise le la hauteur maximale de l'enfant et la somme des largeurs de ses enfants. Il place ensuite ses enfants.
Notez que chaque nœud n'a été visité qu'une seule fois. L'environnement d'exécution Compose ne nécessite qu'un seul passer par l'arborescence de l'UI pour mesurer et placer tous les nœuds, ce qui améliore des performances. Lorsque le nombre de nœuds dans l'arborescence augmente, le temps passé en traversant celle-ci augmente de manière linéaire. En revanche, si chaque nœud était visité plusieurs fois, le temps de balayage augmente de manière exponentielle.
Dessin
Au cours de la phase de dessin, l'arborescence est balayée de haut en bas, Le nœud se dessine à tour de rôle sur l'écran.
Figure 5. La phase de dessin dessine les pixels à l'écran.
Dans l'exemple précédent, le contenu de l'arborescence est dessiné de la manière suivante:
Row
dessine tout le contenu qu'il peut comporter, comme une couleur d'arrière-plan.Image
se dessine lui-même.Column
se dessine lui-même.- Les première et deuxième
Text
se dessinent, respectivement.
Figure 6. Arborescence d'UI avec sa représentation dessinée.
Lectures d'état
Lorsque vous lisez la valeur d'un état d'instantané au cours de l'une des phases répertoriées ci-dessus, Compose suit automatiquement les actions effectuées lors de la lecture de cette valeur. Ce suivi permet à Compose de réexécuter le lecteur lorsque la valeur de l'état change. Il constitue la base de l'observabilité de l'état dans Compose.
L'état est généralement créé à l'aide de mutableStateOf()
, puis accessible de l'une des deux manières suivantes : directement via la propriété value
ou via un délégué de propriété Kotlin. Pour en savoir plus à ce sujet, consultez la section État dans les composables. Pour les besoins de ce guide, une "lecture d'état" fait référence à l'une ou l'autre de ces méthodes d'accès équivalentes.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
En arrière-plan du délégué de propriété, les fonctions "getter" et "setter" permettent d'accéder à la valeur (value
) de l'état et de la mettre à jour. Ces fonctions ne sont appelées que lorsque vous référencez la propriété en tant que valeur, et non lorsqu'elle est créée. C'est pourquoi les deux méthodes ci-dessus sont équivalentes.
Chaque bloc de code pouvant être réexécuté lorsqu'un état de lecture est modifié correspond à un champ d'application de redémarrage. Compose effectue le suivi des changements de valeur d'état et redémarre les champs d'application en plusieurs phases.
Lectures d'état par phases
Comme indiqué ci-dessus, Compose se déroule en trois phases principales, lesquelles permettent de suivre les états lus dans chacune d'elles. Cela permet à Compose de notifier uniquement les phases spécifiques pour lesquelles des tâches sont nécessaires pour chaque élément d'UI concerné.
Passons en revue chaque phase et décrivons ce qui se passe lorsqu'une valeur d'état est lue qu'il contient.
Phase 1 : Composition
Les lectures d'état dans une fonction @Composable
ou un bloc lambda concernent la composition et potentiellement les phases suivantes. Lorsque la valeur d'état change, le recomposeur programme de nouveau l'exécution de toutes les fonctions modulables qui la lisent. Notez que l'environnement d'exécution peut ignorer une partie ou la totalité des fonctions modulables si les entrées n'ont pas changé. Pour en savoir plus, consultez la section Ignorer si les entrées n'ont pas changé.
Selon le résultat de la composition, l'interface utilisateur de Compose exécute les phases de mise en page et de dessin. Elle peut ignorer ces phases si le contenu reste le même, et que la taille et la mise en page ne changent pas.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Phase 2 : Mise en page
La phase de mise en page comprend deux étapes : la mesure et le positionnement. L'étape de mesure exécute le lambda de mesure transmis au composable Layout
, à la méthode MeasureScope.measure
de l'interface LayoutModifier
, etc. L'étape de positionnement exécute le bloc de positionnement de la fonction layout
, le bloc lambda de Modifier.offset { … }
, etc.
Les lectures d'état lors de chacune de ces étapes concernent la mise en page et, potentiellement, la phase de dessin. Lorsque l'état change, l'interface utilisateur de Compose planifie la phase de mise en page. Elle exécute également la phase de dessin si la taille ou la position a changé.
Plus précisément, l'étape de mesure et l'étape de positionnement impliquent des champs d'application de redémarrage distincts. Autrement dit, les lectures d'état dans l'étape de positionnement n'appellent pas l'étape de mesure avant cela. Cependant, ces deux étapes sont souvent liées, de sorte qu'un état lu dans l'étape de positionnement peut avoir une incidence sur les autres champs d'application de redémarrage appartenant à l'étape de mesure.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Phase 3 : Dessin
Les lectures d'état pendant le dessin concernent la phase de dessin. Canvas()
, Modifier.drawBehind
et Modifier.drawWithContent
sont des exemples courants. Lorsque la valeur de l'état change, l'interface utilisateur de Compose n'exécute que la phase de dessin.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Optimiser les lectures d'état
À mesure que Compose effectue le suivi des lectures d'état localisé, la quantité de travail effectuée peut être réduite en lisant chaque état dans une phase appropriée.
Prenons un exemple. Ici, nous avons Image()
, qui utilise le modificateur de décalage pour décaler sa position finale de mise en page, ce qui entraîne un effet de parallaxe lorsque l'utilisateur fait défiler l'écran.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Ce code fonctionne, mais génère des performances non optimales. Il lit la valeur de l'état firstVisibleItemScrollOffset
et la transmet à la fonction Modifier.offset(offset: Dp)
. La valeur firstVisibleItemScrollOffset
change à mesure que l'utilisateur fait défiler l'écran. Comme nous le savons, Compose suit toutes les lectures d'état afin de pouvoir relancer (rappeler) le code de lecture qui, dans notre exemple, correspond au contenu de Box
.
Il s'agit d'un exemple d'état lu pendant la phase de composition. Ce n'est pas nécessairement une mauvaise chose. En réalité, c'est là la base de la recomposition : permettre les modifications de données pour émettre une nouvelle UI.
Cet exemple n'est pas optimal, car chaque événement de défilement entraîne la réévaluation de l'intégralité du contenu composable, ainsi que sa mesure, sa mise en page et son dessin. La phase Compose est déclenchée à chaque défilement, même si ce que nous voyons ne change pas. Par contre, la position change. Nous pouvons optimiser la lecture de l'état pour ne déclencher que la phase de mise en page.
Une autre version du modificateur de décalage est disponible : Modifier.offset(offset: Density.() -> IntOffset)
.
Cette version utilise un paramètre lambda, où le décalage obtenu est renvoyé par le bloc lambda. Mettons à jour le code pour l'utiliser :
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Pourquoi est-il plus performant ? Le bloc lambda que nous fournissons au modificateur est appelé pendant la phase de mise en page (plus spécifiquement lors de l'étape de positionnement pendant la phase de mise en page). Autrement dit, l'état firstVisibleItemScrollOffset
n'est plus lu pendant la composition. Étant donné que Compose suit la lecture de l'état, cette modification signifie que si la valeur firstVisibleItemScrollOffset
change, Compose doit uniquement redémarrer les phases de mise en page et de dessin.
Cet exemple s'appuie sur les différents modificateurs de décalage pour optimiser le code généré, mais l'idée générale reste valide : essayez de limiter les lectures d'état à la phase la plus basse possible afin de permettre à Compose d'effectuer le moins de travail possible.
Certes, il est souvent absolument nécessaire de lire les états dans la phase de composition. Toutefois, il reste possible de réduire le nombre de recompositions à leur minimum en filtrant les changements d'état. Pour en savoir plus à ce sujet, consultez la page derivedStateOf : convertir un ou plusieurs objets d'état en un autre état.
Recomposition en boucle (dépendance de phase cyclique)
Précédemment, nous avons mentionné que les phases de Compose sont toujours appelées dans le même ordre et qu'il n'existe aucun moyen de revenir en arrière dans le même frame. Toutefois, cela n'empêche pas les applications de réaliser des compositions en boucle sur différents frames. Prenons l'exemple suivant :
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
Dans cet exemple, nous avons (mal) implémenté une colonne verticale, avec l'image en haut, puis le texte en dessous. Nous utilisons Modifier.onSizeChanged()
pour identifier la taille résolue de l'image, puis nous utilisons Modifier.padding()
au niveau du texte pour le décaler vers le bas. La conversion non naturelle de Px
en Dp
indique déjà que le code présente un problème.
Le problème avec cet exemple est que nous ne parvenons pas à la mise en page "finale" dans un seule frame. Le code repose sur l'affichage de plusieurs frames, ce qui entraîne un travail inutile et l'instabilité de l'interface utilisateur à l'écran.
Passons en revue chaque frame pour voir ce qui se passe :
Lors de la phase de composition du premier frame, imageHeightPx
a la valeur 0, et le texte est fourni avec Modifier.padding(top = 0)
. S'ensuit la phase de mise en page. Le rappel du modificateur onSizeChanged
est appelé.
C'est à ce moment que l'attribut imageHeightPx
est mis à jour avec la hauteur réelle de l'image.
Compose planifie la recomposition du frame suivant. Au cours de la phase de dessin, le texte est affiché avec une marge intérieure correspondant à 0, car la modification de valeur n'est pas encore reflétée.
Compose démarre ensuite le deuxième frame planifié par le changement de valeur d'imageHeightPx
. L'état est lu dans le bloc de contenu Box et il est appelé lors de la phase de composition. Cette fois, le texte est fourni avec une marge intérieure correspondant à la hauteur de l'image. Lors de la phase de mise en page, le code définit à nouveau la valeur d'imageHeightPx
, mais aucune recomposition n'est planifiée, car la valeur reste la même.
Au final, nous obtenons la marge intérieure souhaitée sur le texte, mais il n'est pas optimal d'utiliser un frame supplémentaire pour transmettre la valeur de la marge intérieure à une autre phase et de générer ainsi un frame dont le contenu se chevauche.
Cet exemple peut sembler artificiel, mais faites attention à ce schéma général :
Modifier.onSizeChanged()
,onGloballyPositioned()
ou autres opérations de mise en page- Mise à jour d'un état
- Utilisation de cet état comme entrée d'un modificateur de mise en page (
padding()
,height()
ou similaire) - Répétition potentielle
Pour résoudre le problème ci-dessus, la solution consiste à utiliser les primitives de mise en page appropriées. L'exemple ci-dessus peut être implémenté avec un simple attribut Column()
, mais vous pouvez disposer d'un exemple plus complexe nécessitant une personnalisation et, par conséquent, une mise en page personnalisée. Pour en savoir plus, consultez le guide Mises en page personnalisées.
Le principe général ici est d'avoir une source unique et fiable pour tous les éléments d'interface utilisateur qui doivent être mesurés et placés les uns par rapport aux autres. Si vous utilisez une primitive de mise en page appropriée ou si vous créez une mise en page personnalisée, le parent partagé minimal sert de source fiable pouvant coordonner la relation entre divers éléments. L'introduction d'un état dynamique enfreint ce principe.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé.
- États et Jetpack Compose
- Listes et grilles
- Kotlin pour Jetpack Compose