Compose exécute un frame en trois phases strictement ordonnées et progressives :
- Composition : exécute les fonctions
@Composablepour créer et mettre à jour l'arborescence de l'UI. - Mise en page : mesure les enfants, puis les positionne.
- Dessin : émet des commandes de dessin de canevas pour afficher les pixels à l'écran.
Chaque fois qu'un State Compose est lu au cours d'une phase, Compose enregistre automatiquement une dépendance entre cet état et la phase correspondante.
Qu'est-ce qui rend une écriture "rétrograde" ?
Une écriture rétrograde se produit chaque fois qu'un état est modifié dans une phase ultérieure (ou dans un champ d'application en aval, c'est-à-dire un champ d'application composable exécuté plus tard dans l'ordre au cours de la même passe de composition) que celle où il a été lu, ce qui force Compose à recomposer en planifiant une phase ou un composable antérieur à exécuter à nouveau. Une écriture vers l'arrière est une boucle de recomposition non optimisée.
Conséquences des rétroécritures
Les écritures inversées ne sont pas nécessairement une mauvaise chose et ne déclenchent pas toujours des plantages, mais elles sont inefficaces et peuvent nuire aux performances de l'application de plusieurs manières :
- Rendu d'images supplémentaires et images supprimées : une écriture rétrograde force Compose à exécuter des passes de composition redondantes sur des images consécutives, ce qui gaspille les ressources du processeur et du GPU, et peut entraîner des à-coups.
- Problèmes d'exactitude de la première image : si votre composant nécessite une écriture rétroactive pour résoudre ses dimensions ou son état finaux, la première image est affichée avec des données non valides, par défaut ou non définies (par exemple, une taille nulle ou un décalage incorrect). Cela entraîne un pop-up visuel visible ou un clignotement de la mise en page lors de l'affichage de la deuxième image.
- Boucles de recomposition infinies : si un changement d'état modifie la taille de la mise en page et que la taille de la mise en page réécrit continuellement une nouvelle valeur dans l'état, vous risquez de créer une boucle de frames infinie dans laquelle l'écran se recompose constamment à chaque frame sans jamais se stabiliser.
Flux de transfert entre les phases
Les changements d'état doivent toujours se faire dans l'ordre des phases :
| Phase de lecture | Write Context | Acceptable ? | Pourquoi ? |
|---|---|---|---|
Mise en page (Modifier.offset { }) |
Composition | Oui | La composition met à jour l'état → La mise en page le lit plus tard dans le même frame sans recomposition. |
Match nul (graphicsLayer { }, drawBehind { }) |
Composition | Oui | La composition met à jour l'état → Draw le lit lors de la phase finale. La composition et la mise en page sont complètement ignorées. |
| Match nul | Disposition | Oui | La mise en page met à jour l'état, et le dessin le lit, ce qui est un flux valide. |
| Composition | Rappel d'événement (onClick, onValueChange) entraînant un changement d'état. Remarque : Les rappels de mise en page ne sont pas comptabilisés comme des événements. |
Oui | Un événement modifie l'état utilisé pour piloter la composition. Si l'événement se produit hors du frame (pas dans Composition, Layout ou Draw), cela est valide. |
| Composition | Coroutine (LaunchedEffect) |
Oui, avec prudence | Met à jour l'état de manière asynchrone en réponse au cycle de vie/aux événements. Les écritures à partir d'effets peuvent être valides, mais peuvent indiquer une superposition d'états inefficace. Cela doit être évité autant que possible. |
| Emplacement (dans la mise en page) | Mesure (dans la mise en page) | Oui | Dans Layout, il est acceptable de mettre à jour l'état, puis de le lire dans le placement. |
| Mesurer (dans la mise en page) | Emplacement (dans la mise en page) | Non, écriture inversée | L'écriture dans l'état de placement, qui est ensuite lue dans l'état de lecture, entraîne une boucle de remesure. |
| Composition | Mise en page (onSizeChanged, LayoutModifier) |
Non : écriture à l'envers | La mise en page invalide la boucle Composition → Recomposition. |
| Composition | Match nul (drawWithContent, Canvas) |
Non : écriture inversée | Le dessin invalide la boucle Composition → Recomposition. |
Combinaisons de phases : arrière et avant
Voici des exemples d'écritures inversées dans Compose et comment les résoudre.
En arrière : lecture dans Composition, écriture dans Layout
- Que se passe-t-il ? La composition lit
componentHeightpour déterminer l'UI à émettre. Plus tard dans l'image, la phase de mise en page mesure ou place les vues et écrit une nouvelle valeur danscomponentHeight(par exemple, à l'aide deonSizeChanged,onGloballyPositionedou d'unLayoutModifierpersonnalisé). - Résultat : la modification de
componentHeightdans la phase de mise en page invalide la phase de composition qui vient de se terminer. Notez queonSizeChangedindique la taille une fois la mesure de la mise en page terminée. Si la valeur d'état mise à jour se stabilise lors du passage suivant, la recomposition peut s'arrêter après une frame supplémentaire. Toutefois, si la nouvelle valeur continue de modifier la taille, cela entraîne une boucle de frames infinie. De plus,onGloballyPositioneds'exécute après la mise en page et l'emplacement, ce qui rend les écritures d'état encore plus susceptibles de provoquer des boucles de recomposition et de mise en page continues sur des frames consécutifs.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
En arrière : lecture dans la composition, écriture dans le dessin
- Que se passe-t-il ? L'état est lu dans le corps du composable (phase de composition), mais modifié dans
Modifier.drawWithContent,Modifier.drawBehindouCanvas(phase de dessin). - Résultat : la phase de dessin modifie l'état → Composition invalidée → Boucle sans fin.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
Rétrograde : lecture dans la composition, écriture dans la composition (même phase)
- Que se passe-t-il : lecture de
countdans la fonction composable et modification decountdirectement dans l'emplacement de contenu d'un autre composable après l'avoir lu. - Résultat : le système d'instantanés enregistre la lecture et l'écriture suivante dans la même passe de composition, ce qui invalide immédiatement le champ d'application actuel.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
Règles clés pour éviter les rétroécritures
- N'écrivez pas dans l'état dans
onGloballyPositioned,onSizeChanged,onPlaced,onLayoutRectChangedouLayoutModifiersi cet état est lu dans la composition, car cela entraîne un problème d'exactitude de la première image.- Si les coordonnées ou les tailles de mise en page ne sont nécessaires que pour un dessin personnalisé, lisez-les directement dans la phase de dessin ou de mise en page (par exemple, à l'aide de
Modifier.drawWithCacheouModifier.layout). - Pour le dimensionnement au niveau de la fenêtre (
WindowWidthSizeClass) : hissez l'observation de la taille au niveau de la fenêtre. La composition se divise en classes de taille de fenêtre avant que la mesure locale ne se produise. - Maintenez l'uniformité de la composition : utilisez une seule mise en page personnalisée ou des composants tels que
FlowRowouLazyVerticalGridqui ajustent la mesure et l'emplacement lors de la phase 2 sans recomposer ni modifier l'état utilisé dans la composition. - Utiliser la sous-composition : utilisez
BoxWithConstraintsouSubcomposeLayoutlorsque les composables enfants doivent se ramifier en fonction de la largeur ou de la hauteur locale. À utiliser avec précaution : la sous-composition entraîne un coût en termes de performances et peut généralement être évitée. - En dernier recours : autorisez la première image à être incorrecte, en stockant la taille dans
onSizeChangedpour déclencher une deuxième recomposition d'image. Cela entraîne des problèmes de mise en page visibles, des à-coups et des risques de boucles infinies.
- Si les coordonnées ou les tailles de mise en page ne sont nécessaires que pour un dessin personnalisé, lisez-les directement dans la phase de dessin ou de mise en page (par exemple, à l'aide de
- Ne modifiez pas l'état après sa première lecture dans la composition :
- Bien que vous puissiez écrire en toute sécurité dans des objets
MutableStatependant la composition en dehors d'unSideEffect, veillez à ne pas écrire dans un état que vous avez peut-être déjà lu dans la composition. Il est recommandé d'utiliserrememberUpdatedStatelorsque vous devez écrire un état dans la composition. Écrire dans un état pendant la composition d'une autre manière est généralement le signe d'un effet manquant ou d'un état ou d'un composable mal conçu. N'oubliez pas que la composition est optimiste et s'exécute toujours avec la dernière valeur d'un état. Il est donc possible que vous ne voyiez pas tous les changements d'état dans les recompositions. Les mises à jour de l'UI ne doivent pas être utilisées pour gérer des événements ponctuels. Il est donc rare que la valeur d'un état doive être mise à jour à la suite d'une recomposition. - Évitez de modifier les états observés en dehors de la composition (par exemple, les champs
ViewModelou les indicateursisVisible). Les écritures d'état qui affectent la composition appartiennent aux lambdas d'événement (onClick), aux coroutines (LaunchedEffect) ou aux effets secondaires (SideEffect).rememberUpdatedStateest une exception, car il est conçu pour modifier l'état qui n'est utilisé que dans le corps de@Composable.
- Bien que vous puissiez écrire en toute sécurité dans des objets
- Différez les lectures d'état à la dernière phase possible :
- La lecture des états dans Draw (
Modifier.graphicsLayer { alpha = ... }) ou Layout (Modifier.offset { IntOffset(...) }) garantit que les modifications n'invalident que la phase 2 ou 3, en ignorant complètement la phase 1 (Composition).
- La lecture des états dans Draw (