Compose ejecuta un fotograma en tres fases ordenadas de forma estricta y con flujo hacia delante:
- Composición: Ejecuta funciones
@Composablepara compilar y actualizar el árbol de la IU. - Diseño: Mide los elementos secundarios y, luego, los ubica.
- Dibujo: Emite comandos de dibujo de lienzo para renderizar píxeles en la pantalla.
Cada vez que se lee un State de Compose durante cualquier fase, Compose registra automáticamente una dependencia entre ese estado y la fase correspondiente.
¿Qué hace que una escritura sea "hacia atrás"?
Se produce una escritura hacia atrás cada vez que se modifica un estado en una fase posterior (o en un alcance descendente, es decir, un alcance de componibilidad que se ejecuta más adelante en orden dentro del mismo pase de composición) que donde se leyó, lo que obliga a Compose a volver a componer programando una fase o un elemento componible anteriores para que se vuelvan a ejecutar. Una escritura hacia atrás es un bucle de recomposición no optimizado.
Consecuencias de las escrituras hacia atrás
Las escrituras hacia atrás no son necesariamente algo malo y no siempre provocan fallas, pero son ineficientes y pueden perjudicar el rendimiento de la app de varias maneras:
- Renderización de fotogramas adicionales y fotogramas perdidos: Una escritura hacia atrás obliga a Compose a ejecutar pases de composición redundantes en fotogramas consecutivos, lo que desperdicia recursos de CPU y GPU, y puede causar bloqueos.
- Problemas de corrección del primer fotograma: Si tu componente requiere una escritura hacia atrás para resolver sus dimensiones o estado finales, el primer fotograma se renderiza con datos no válidos, predeterminados o inestables (como un tamaño cero o un desplazamiento incorrecto). Esto provoca un parpadeo visual o un destello de diseño visibles cuando se renderiza el segundo fotograma.
- Bucles de recomposición infinitos: Si un cambio de estado altera el tamaño del diseño y este escribe continuamente un valor nuevo en el estado, corres el riesgo de crear un bucle de fotogramas infinito en el que la pantalla se recompone constantemente en cada fotograma sin estabilizarse.
Flujo hacia adelante a través de las fases
Los cambios de estado siempre deben fluir hacia adelante a través de las fases:
| Fase de lectura | Write Context | ¿Es aceptable? | Por qué |
|---|---|---|---|
Diseño (Modifier.offset { }) |
Composición | Sí | La composición actualiza el estado → El diseño lo lee más tarde en el mismo fotograma sin volver a componer. |
Empate (graphicsLayer { }, drawBehind { }) |
Composición | Sí | La composición actualiza el estado → El dibujo lo lee en la fase final. La composición y el diseño se omiten por completo. |
| Dibuja | Diseño | Sí | El diseño actualiza el estado y el dibujo lo lee, flujo válido. |
| Composición | Devolución de llamada de evento (onClick, onValueChange) que genera un cambio de estado. Nota: Las devoluciones de llamada de diseño no se consideran eventos. |
Sí | Un evento muta el estado que se usa para impulsar la composición. Si el evento ocurre fuera del fotograma (no en Composition, Layout ni Draw), es válido. |
| Composición | Corrutina (LaunchedEffect) |
Sí, con precaución | Actualiza el estado de forma asíncrona en respuesta al ciclo de vida o los eventos. Las escrituras de efectos pueden ser válidas, pero pueden apuntar a una estratificación de estado ineficiente. Se deben evitar siempre que sea posible. |
| Posición (en el diseño) | Medición (en el diseño) | Sí | En Layout, es aceptable actualizar el estado y, luego, leerlo en la posición. |
| Medición (en el diseño) | Posición (en el diseño) | No, escritura hacia atrás | Escribir en el estado de posición que luego se encuentra más adelante en el estado de lectura provoca un bucle de nueva medición. |
| Composición | Diseño (onSizeChanged, LayoutModifier) |
No: Escritura hacia atrás | El diseño invalida la composición → Bucle de recomposición. |
| Composición | Empate (drawWithContent, Canvas) |
No, escritura hacia atrás | El dibujo invalida la composición → Bucle de recomposición. |
Combinaciones de fases: hacia atrás frente a hacia adelante
A continuación, se muestran ejemplos de escrituras hacia atrás en Compose y cómo puedes resolverlas.
Hacia atrás: Lectura en la composición y escritura en el diseño
- Qué sucede: La composición lee
componentHeightpara determinar qué IU emitir. Más adelante en el fotograma, la fase de diseño mide o coloca vistas y escribe un valor nuevo encomponentHeight(por ejemplo, cononSizeChanged,onGloballyPositionedoLayoutModifierpersonalizado). - Resultado: Modificar
componentHeighten el diseño invalida la fase de composición que se acaba de completar. Ten en cuenta queonSizeChangedinforma el tamaño después de que se completa el pase de medición del diseño. Si el valor de estado actualizado se estabiliza en el siguiente pase, la recomposición puede detenerse después de un fotograma adicional. Sin embargo, si el valor nuevo sigue alterando el tamaño, se produce un bucle de fotogramas infinito. Además,onGloballyPositionedse ejecuta después del diseño y la ubicación, lo que hace que las escrituras de estado dentro de él sean aún más susceptibles a los bucles continuos de recomposición y rediseño en fotogramas consecutivos.
// ❌ 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) } } }
Hacia atrás: Lectura en la composición y escritura en el dibujo
- Qué sucede: El estado se lee en el cuerpo de la función de componibilidad (fase de composición), pero muta dentro de
Modifier.drawWithContent,Modifier.drawBehindoCanvas(fase de dibujo). - Resultado: La fase de dibujo muta el estado → Composición invalidada → bucle sin 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 } ) }
Hacia atrás: Lectura en la composición, escritura en la composición (misma fase)
- Qué sucede: Se lee
counten la función de componibilidad y se modificacountdirectamente en otra ranura de contenido de elementos de componibilidad después de leerlo. - Resultado: El sistema de instantáneas registra la lectura y la escritura posterior dentro del mismo pase de composición, lo que invalida de inmediato el alcance actual.
// ❌ 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 }
Reglas clave para evitar las escrituras hacia atrás
- No escribas en el estado en
onGloballyPositioned,onSizeChanged,onPlaced,onLayoutRectChangedniLayoutModifiersi ese estado se lee en Composition, ya que esto causa el problema de corrección del primer fotograma.- Si las coordenadas o los tamaños del diseño solo se necesitan para el dibujo personalizado, léelos directamente en la fase de dibujo o diseño (por ejemplo, con
Modifier.drawWithCacheoModifier.layout). - Para el ajuste de tamaño a nivel de la ventana (
WindowWidthSizeClass): Eleva la observación del tamaño al nivel de la ventana. La composición se ramifica en clases de tamaño de ventana antes de que se realice la medición local. - Mantén la composición uniforme: Usa un solo diseño personalizado o componentes como
FlowRowoLazyVerticalGridque ajusten la medición y la ubicación durante la fase 2 sin volver a componer ni alterar el estado usado en la composición. - Usa la subcomposición: Usa
BoxWithConstraintsoSubcomposeLayoutcuando las funciones de componibilidad secundarias deban ramificarse según el ancho o la altura locales. Ten cuidado: La subcomposición tiene un costo de rendimiento y, por lo general, se puede evitar. - Como último recurso: Permite que el primer fotograma sea incorrecto y almacena el tamaño en
onSizeChangedpara activar una segunda recomposición de fotogramas. Esto provoca un salto visible en el diseño, bloqueos y riesgos de bucles infinitos.
- Si las coordenadas o los tamaños del diseño solo se necesitan para el dibujo personalizado, léelos directamente en la fase de dibujo o diseño (por ejemplo, con
- No cambies el estado después de que se lea por primera vez en la composición:
- Si bien puedes escribir de forma segura en objetos
MutableStatedurante la composición fuera de unSideEffect, ten especial cuidado para asegurarte de no escribir en un estado que hayas leído previamente en la composición. Se recomienda usarrememberUpdatedStatecuando surge esta necesidad de escribir un estado en la composición. Escribir en un estado durante la composición de otra manera suele ser un signo de un efecto faltante o de un estado o elemento componible diseñado de forma inapropiada. Recuerda que la composición es optimista y siempre se ejecuta con el valor más reciente de un estado, por lo que es posible que no veas todos los cambios de estado en las recomposiciones. Las actualizaciones de la IU no deben usarse como una forma de controlar eventos únicos, lo que hace que sea poco común que el valor de un estado deba actualizarse como resultado de una recomposición. - Evita mutar los estados que se observan fuera de la composición (por ejemplo, los campos
ViewModelo las marcasisVisible). Las escrituras de estado que afectan la composición pertenecen a lambdas de eventos (onClick), corrutinas (LaunchedEffect) o efectos secundarios (SideEffect).rememberUpdatedStatees una excepción, ya que está diseñada para mutar el estado que solo se usa en el cuerpo de@Composable.
- Si bien puedes escribir de forma segura en objetos
- Aplazar las lecturas de estado a la última fase posible:
- La lectura de estados en Draw (
Modifier.graphicsLayer { alpha = ... }) o Layout (Modifier.offset { IntOffset(...) }) garantiza que los cambios solo invaliden la fase 2 o 3, y se omita por completo la fase 1 (composición).
- La lectura de estados en Draw (