Compose exécute un frame en trois phases strictement ordonnées et séquentielles :
- Composition : exécute des fonctions
@Composablepour créer et mettre à jour l'arborescence de l'UI. - Mise en page : mesure les enfants, puis les place.
- Drawing : émet des commandes de dessin du 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 qu'une écriture "à l'envers" ?
Une écriture en arrière se produit chaque fois qu'un état est modifié dans une phase ultérieure (ou dans une portée en aval, c'est-à-dire une portée composable exécutée plus tard dans l'ordre au cours de la même passe de composition) que celle où il a été lu, ce qui oblige Compose à recomposer en planifiant l'exécution d'une phase ou d'un composable antérieur. Une écriture à l'envers est une boucle de recomposition non optimisée.
Conséquences des écritures en arrière
Les écritures à l'envers 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 façons :
- Rendu de frames supplémentaires et frames abandonnés : une écriture en arrière oblige Compose à exécuter des passes de composition redondantes sur des frames consécutifs, ce qui gaspille les ressources du CPU et du GPU, et peut potentiellement provoquer des saccades.
- Problèmes de correction de la première frame : si votre composant nécessite une écriture en arrière pour résoudre ses dimensions ou son état finaux, la première frame est rendue avec des données non valides, par défaut ou non définies (telles qu'une taille nulle ou un décalage incorrect). Cela provoque un scintillement ou un clignotement visible de la mise en page lorsque le deuxième frame s'affiche.
- 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 en permanence une nouvelle valeur dans l'état, vous risquez de créer une boucle de frames infinie où l'écran se recompose constamment à chaque frame sans jamais se stabiliser.
Flux de transfert à travers les phases
Les changements d'état doivent toujours s'effectuer vers l'avant dans les phases :
| Phase de lecture | Écrire le contexte | 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 recomposer. |
Tirage (graphicsLayer { }, drawBehind { }) |
Composition | Oui | La composition met à jour l'état → Draw le lit dans la phase finale. La composition et la mise en page sont entièrement ignorées. |
| Match nul | Disposition | Oui | La mise en page met à jour l'état, et le dessin le lit. Le flux est valide. |
| Composition | Rappel d'événement (onClick, onValueChange) entraînant un changement d'état. Remarque : Les rappels de mise en page ne sont pas considéré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 cadre (pas dans "Composition", "Mise en page" ou "Dessin"), il est valide. |
| Composition | Coroutine (LaunchedEffect) |
Oui, avec précaution | 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. Elles doivent être évitées autant que possible. |
| Emplacement (dans la mise en page) | Mesurer (dans la mise en page) | Oui | Dans Layout, il est acceptable de mettre à jour l'état, puis de le lire dans l'emplacement. |
| Mesurer (dans la mise en page) | Emplacement (dans la mise en page) | Non : écriture à l'envers | L'écriture dans l'état de placement, qui est ensuite lue dans l'état de lecture, entraîne une boucle de remesure. |
| Composition | Disposition (onSizeChanged, LayoutModifier) |
Non : Écrire à l'envers | La mise en page invalide la composition → boucle de recomposition. |
| Composition | Tirage (drawWithContent, Canvas) |
Non : écriture à l'envers | Draw invalide la boucle Composition → Recomposition. |
Combinaisons de phases : vers l'arrière ou vers l'avant
Vous trouverez ci-dessous des exemples d'écritures en arrière dans Compose et comment les résoudre.
Inversé : lecture dans Composition, écriture dans Mise en page
- Ce qui se passe : la composition lit
componentHeightpour déterminer l'UI à émettre. Plus tard dans le frame, la phase de mise en page mesure ou place les vues et écrit une nouvelle valeur danscomponentHeight(par exemple, à l'aide deonSizeChanged,onGloballyPositionedouLayoutModifierpersonnalisé). - Résultat : la modification de
componentHeightdans la mise en page invalide la phase de composition qui vient d'être effectuée. Notez queonSizeChangedindique la taille une fois la passe de mesure de la mise en page terminée. Si la valeur d'état mise à jour se stabilise au prochain passage, la recomposition peut s'arrêter après un 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 le placement, ce qui rend les écritures d'état à l'intérieur encore plus susceptibles de boucles de recomposition et de réorganisation continues sur plusieurs 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) } } }
Inversé : lecture dans Composition, écriture dans Dessin
- Ce qui se passe : l'état est lu dans le corps composable (phase de composition), mais muté à l'intérieur de
Modifier.drawWithContent,Modifier.drawBehindouCanvas(phase de dessin). - Résultat : la phase de dessin modifie l'état → la composition est 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 } ) }
En arrière : lire dans la phase de composition, écrire dans la phase de composition (même phase)
- Ce qui se passe : lecture de
countdans la fonction composable et modification decountdirectement dans l'emplacement de contenu d'un autre composable après la lecture. - Résultat : Le système d'instantanés enregistre la lecture et l'écriture ultérieure 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,onSizeChangedouLayoutModifiersi cet état est lu dans Composition, car cela entraîne le problème de correction de la première frame.- Si les coordonnées ou les tailles de mise en page ne sont nécessaires que pour le 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 la taille de la fenêtre au niveau de la fenêtre. La composition se ramifie en fonction des classes de taille de fenêtre avant la mesure locale. - Conserver une composition uniforme : utilisez une seule mise en page personnalisée ou des composants tels que
FlowRowouLazyVerticalGridqui ajustent la mesure et le placement pendant 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 locales. Attention : La sous-composition a un coût en termes de performances et peut généralement être évitée. - En dernier recours : autorisez le premier frame à être incorrect, en stockant la taille dans
onSizeChangedpour déclencher une deuxième recomposition du frame. Cela provoque des sauts de mise en page visibles, des saccades et des risques de boucles infinies.
- Si les coordonnées ou les tailles de mise en page ne sont nécessaires que pour le 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
MutableStatelors de la composition en dehors d'unSideEffect, veillez tout particulièrement à ne pas écrire dans un état que vous avez peut-être lu précédemment dans la composition. Il est recommandé d'utiliserrememberUpdatedStatelorsque ce besoin d'écriture d'état dans la composition se présente. É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çus. 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@Composable.
- Bien que vous puissiez écrire en toute sécurité dans des objets
- Différez les lectures d'état jusqu'à 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 les phases 2 ou 3, en ignorant complètement la phase 1 (Composition).
- La lecture des états dans Draw (