O Compose executa um frame em três fases estritamente ordenadas e de fluxo progressivo:
- Composição: executa funções
@Composablepara criar e atualizar a árvore da interface. - Layout: mede os filhos e os posiciona.
- Desenho: emite comandos de desenho de tela para renderizar pixels na tela.
Sempre que um State do Compose é lido durante qualquer fase, o Compose registra automaticamente uma dependência entre esse estado e a fase correspondente.
O que torna uma gravação "reversa"?
Uma gravação reversa ocorre sempre que um estado é modificado em uma fase posterior (ou em um escopo downstream, ou seja, um escopo de composição executado posteriormente em ordem na mesma passagem de composição) em relação ao local em que foi lido, forçando o Compose a fazer a recomposição programando uma fase anterior ou um elemento de composição para ser executado novamente. Uma gravação reversa é um loop de recomposição não otimizado.
Consequências das gravações inversas
As gravações reversas não são necessariamente ruins e nem sempre causam falhas, mas são ineficientes e podem prejudicar o desempenho do app de várias maneiras:
- Renderização de frames extras e frames perdidos: uma gravação reversa força o Compose a executar passagens de composição redundantes em frames consecutivos, desperdiçando recursos de CPU e GPU e possivelmente causando instabilidade.
- Problemas de correção do primeiro frame: se o componente exigir uma gravação reversa para resolver as dimensões ou o estado final, o primeiro frame será renderizado com dados inválidos, padrão ou não resolvidos, como tamanho zero ou deslocamento incorreto. Isso causa um pop-up visual visível ou uma oscilação de layout quando o segundo frame é renderizado.
- Loops de recomposição infinitos: se uma mudança de estado alterar o dimensionamento do layout, e o dimensionamento do layout gravar continuamente um novo valor no estado, você corre o risco de criar um loop de frame infinito em que a tela se recompõe constantemente a cada frame sem nunca se estabilizar.
Fluxo de encaminhamento pelas fases
As mudanças de estado precisam sempre seguir em frente pelas fases:
| Fase de leitura | Contexto de gravação | Aceitável? | Por quê |
|---|---|---|---|
Layout (Modifier.offset { }) |
Composição | Sim | A composição atualiza o estado → o layout o lê mais tarde no mesmo frame sem recomposição. |
Empate (graphicsLayer { }, drawBehind { }) |
Composição | Sim | A composição atualiza o estado → O desenho o lê na fase final. As fases de composição e layout são ignoradas. |
| Desenhar | Layout | Sim | O layout atualiza o estado, e o desenho o lê, fluxo válido. |
| Composição | Callback de evento (onClick, onValueChange) que gera uma mudança de estado. Observação: os callbacks de layout não contam como eventos. |
Sim | Um evento altera o estado usado para gerar a composição. Se o evento acontecer fora do frame (não em Composition, Layout ou Draw), isso é válido. |
| Composição | Corrotina (LaunchedEffect) |
Sim, com cuidado | Atualiza o estado de forma assíncrona em resposta ao ciclo de vida/eventos. As gravações de efeitos podem ser válidas, mas podem indicar uma camada de estado ineficiente. Elas devem ser evitadas sempre que possível. |
| Posição (no layout) | Medição (no layout) | Sim | No Layout, é aceitável atualizar o estado e ler esse estado no posicionamento. |
| Medição (no layout) | Posição (no layout) | Não, gravação reversa | A gravação no estado no posicionamento que está mais adiante no estado de leitura causa um loop de nova medição. |
| Composição | Layout (onSizeChanged, LayoutModifier) |
Não: gravação para trás | O layout invalida o loop de composição → recomposição. |
| Composição | Empate (drawWithContent, Canvas) |
Não - Escrita invertida | O desenho invalida a composição → loop de recomposição. |
Combinações de fases: para trás x para frente
Confira a seguir exemplos de gravações reversas no Compose e como resolvê-las.
Para trás: leitura na composição, gravação no layout
- O que acontece: a composição lê
componentHeightpara determinar qual interface emitir. Mais adiante no frame, a fase de layout mede ou posiciona as visualizações e grava um novo valor emcomponentHeight(por exemplo, usandoonSizeChanged,onGloballyPositionedouLayoutModifierpersonalizado). - Resultado: modificar
componentHeightno layout invalida a fase de composição que acabou de ser concluída. OonSizeChangedinforma o tamanho após a conclusão da transmissão de medição do layout. Se o valor do estado atualizado se estabilizar na próxima passagem, a recomposição poderá ser interrompida após um frame extra. No entanto, se o novo valor continuar alterando o tamanho, isso vai resultar em um loop de frame infinito. Além disso, oonGloballyPositionedé executado após o layout e o posicionamento, tornando as gravações de estado dentro dele ainda mais suscetíveis a loops contínuos de recomposição e relayout em frames 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) } } }
Inverso: leitura em Composition, gravação em Drawing
- O que acontece: o estado é lido no corpo do elemento combinável (fase de composição),
mas sofre mutação dentro de
Modifier.drawWithContent,Modifier.drawBehindouCanvas(fase de exibição). - Resultado: a fase de desenho muda o estado → composição invalidada → loop infinito.
// ❌ 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 } ) }
Para trás: leitura na composição, gravação na composição (mesma fase)
- O que acontece: leitura de
countna função combinável e modificação decountdiretamente em outro slot de conteúdo de elementos combináveis após a leitura. - Resultado: o sistema de snapshot registra a leitura e a gravação subsequente na mesma transmissão de composição, invalidando imediatamente o escopo atual.
// ❌ 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 }
Regras principais para evitar gravações inversas
- Não grave no estado em
onGloballyPositioned,onSizeChanged,onPlaced,onLayoutRectChangedouLayoutModifierse esse estado for lido na composição, porque isso causa o problema de correção do primeiro frame.- Se as coordenadas ou os tamanhos do layout forem necessários apenas para desenhos personalizados, leia-os
diretamente na fase de desenho ou layout (por exemplo, usando
Modifier.drawWithCacheouModifier.layout). - Para dimensionamento no nível da janela (
WindowWidthSizeClass): eleve a observação de tamanho para o nível da janela. A composição se ramifica em classes de tamanho de janela antes que a medição local ocorra. - Mantenha a composição uniforme:use um único layout personalizado ou componentes
como
FlowRowouLazyVerticalGridque ajustam a medição e o posicionamento durante a Fase 2 sem recompor ou alterar o estado usado na composição. - Use a subcomposição:use
BoxWithConstraintsouSubcomposeLayoutquando as composições filhas precisarem se ramificar com base na largura ou altura local. Tenha cuidado: a subcomposição tem um custo de desempenho e geralmente pode ser evitada. - Como último recurso:permita que o primeiro frame esteja errado, armazenando o tamanho em
onSizeChangedpara acionar uma segunda recomposição de frame. Isso causa pop-ups de layout visíveis, instabilidade e riscos de loops infinitos.
- Se as coordenadas ou os tamanhos do layout forem necessários apenas para desenhos personalizados, leia-os
diretamente na fase de desenho ou layout (por exemplo, usando
- Não mude o estado depois que ele for lido pela primeira vez na composição:
- Embora seja seguro gravar em objetos
MutableStatedurante a composição fora de umSideEffect, tome cuidado especial para garantir que você não grave em um estado que possa ter sido lido anteriormente na composição. É recomendável usarrememberUpdatedStatequando essa necessidade de uma gravação de estado na composição surgir. Gravar em um estado durante a composição de outra maneira geralmente é um sinal de um efeito ausente ou de um estado ou elemento combinável projetado de forma inadequada. Lembre-se de que a composição é otimista e sempre é executada com o valor mais recente de um estado. Portanto, talvez você não veja todas as mudanças de estado nas recomposições. As atualizações da IU não podem ser usadas como uma forma de processar eventos únicos, o que torna incomum que o valor de um estado precise ser atualizado como resultado da recomposição. - Evite modificar estados que são observados fora da composição (por
exemplo, campos
ViewModelou flagsisVisible). As gravações de estado que afetam a composição pertencem a lambdas de evento (onClick), corrotinas (LaunchedEffect) ou efeitos colaterais (SideEffect).rememberUpdatedStateé uma exceção, já que foi projetado para mudar o estado usado apenas no corpo de@Composable.
- Embora seja seguro gravar em objetos
- Adie as leituras de estado para a fase mais recente possível:
- A leitura de estados em Draw (
Modifier.graphicsLayer { alpha = ... }) ou Layout (Modifier.offset { IntOffset(...) }) garante que as mudanças invalidem apenas a Fase 2 ou 3, ignorando completamente a Fase 1 (Composição).
- A leitura de estados em Draw (