O Compose executa um frame em três fases estritamente ordenadas e de fluxo direto:
- Composição: executa funções
@Composablepara criar e atualizar a árvore de interface. - Layout: mede e posiciona os filhos.
- Desenho: emite comandos de desenho de tela para renderizar pixels na tela.
Sempre que um State do Compose é lido durante qualquer fase, o Compose automaticamente
registra uma dependência entre esse estado e a fase correspondente.
O que faz uma gravação ser "para trás"?
Uma gravação regressiva ocorre sempre que um estado é modificado em uma fase posterior (ou em um escopo downstream, ou seja, um escopo combinável executado posteriormente em ordem na mesma transmissão de composição) do que onde foi lido, forçando o Compose a fazer a recomposição agendando uma fase ou um elemento combinável anterior para ser executado novamente. Uma gravação para trás é um loop de recomposição não otimizado.
Consequências das gravações invertidas
As gravações regressivas 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 descartados: uma gravação para trás força o Compose a executar transmissões 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 para trás para resolver as dimensões ou o estado final, o primeiro frame será renderizado com dados inválidos, padrão ou não definidos (como tamanho zero ou um deslocamento incorreto). Isso causa um efeito visual de estouro ou layout piscando quando o segundo frame é renderizado.
- Loops de recomposição infinita: se uma mudança de estado alterar o dimensionamento do layout e o dimensionamento do layout gravar continuamente um novo valor no estado, você poderá criar um loop de frames infinito em que a tela se recompõe constantemente a cada frame sem nunca estabilizar.
Fluxo de encaminhamento por fases
As mudanças de estado sempre devem fluir para frente pelas fases:
| Fase de leitura | Escrever contexto | Aceitável? | Por quê? |
|---|---|---|---|
Layout (Modifier.offset { }) |
Composição | Sim | A composição atualiza o estado → o layout lê mais tarde no mesmo frame sem recompor. |
Concurso (graphicsLayer { }, drawBehind { }) |
Composição | Sim | A composição atualiza o estado → o desenho o lê na fase final. A composição e o layout são ignorados completamente. |
| Desenhar | Layout | Sim | O layout atualiza o estado, e o desenho o lê, fluxo válido. |
| Composição | Callback de evento (onClick, onValueChange) que impulsiona a mudança de estado. Observação: callbacks de layout não são considerados eventos. |
Sim | Um evento muda o estado usado para acionar a composição. Se o evento acontecer fora do frame (não em Composição, Layout ou Desenho), isso será 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. Evite fazer isso 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 invertida | Escrever no estado em uma posição que está mais adiante no estado de leitura causa um loop de nova medição. |
| Composição | Layout (onSizeChanged, LayoutModifier) |
Não: escrita ao contrário | O layout invalida o loop de composição → recomposição. |
| Composição | Concurso (drawWithContent, Canvas) |
Não - gravação 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 retroativas no Compose e como resolvê-las.
Para trás: leitura na composição, escrita no layout
- O que acontece: a composição lê
componentHeightpara determinar qual interface emitir. Mais tarde no frame, a fase de layout mede ou posiciona 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 depois que a medição do layout é concluída. Se o valor do estado atualizado se estabilizar na próxima transmissão, a recomposição poderá ser interrompida após um frame extra. No entanto, se o novo valor continuar alterando o tamanho, isso resultará em um loop de frames infinito. Além disso,onGloballyPositionedé executado após o layout e o posicionamento, tornando as gravações de estado ainda mais suscetíveis a loops contínuos de recomposição e redefinição de layout 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) } } }
De trás para frente: leitura em Composição e escrita em Desenho
- O que acontece: o estado é lido no corpo combinável (fase de composição),
mas sofre mutação em
Modifier.drawWithContent,Modifier.drawBehindouCanvas(fase de desenho). - 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 e 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 combinável 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 para o estado em
onGloballyPositioned,onSizeChangedouLayoutModifierse esse estado for lido na composição, porque isso causa o problema de correção do primeiro frame.- Se as coordenadas ou tamanhos de layout forem necessários apenas para desenho personalizado, 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 do tamanho do hoist ao nível da janela. A ramificação da composição ocorre em classes de tamanho de janela antes da medição local. - 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. - Usar subcomposição:use
BoxWithConstraintsouSubcomposeLayoutquando os elementos combináveis filhos precisarem ramificar com base na largura ou altura local. Use com cuidado: a subcomposição tem um custo de performance 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 layout visível, instabilidade e riscos de loops infinitos.
- Se as coordenadas ou tamanhos de layout forem necessários apenas para desenho personalizado, leia-os diretamente na fase de desenho ou layout (por exemplo, usando
- Não faça mutações no estado depois que ele for lido pela primeira vez na composição:
- Embora seja possível gravar com segurança 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 lido anteriormente em uma composição. É recomendável usarrememberUpdatedStatequando essa necessidade de gravação de estado na composição surgir. Escrever em um estado durante a composição de outra forma geralmente é um sinal de um efeito ausente ou um estado ou elemento combinável projetado de maneira 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 interface não devem 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 mutações de estados observados fora da composição (por exemplo, campos
ViewModelou flagsisVisible). As gravações de estado que afetam a composição pertencem a lambdas de eventos (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 possível gravar com segurança 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 só invalidem a Fase 2 ou 3, pulando completamente a Fase 1 (Composição).
- A leitura de estados em Draw (