Compose esegue un frame in tre fasi rigorosamente ordinate e in sequenza:
- Composizione: esegue le funzioni
@Composableper creare e aggiornare l'albero della UI. - Layout: misura gli elementi secondari e poi li posiziona.
- Disegno: emette comandi di disegno del canvas per eseguire il rendering dei pixel sullo schermo.
Ogni volta che un State di Compose viene letto durante una fase, Compose registra automaticamente una dipendenza tra lo stato e la fase corrispondente.
Cosa rende una scrittura "all'indietro"?
Una scrittura inversa si verifica ogni volta che uno stato viene modificato in una fase successiva (o in un ambito downstream, ovvero un ambito componibile eseguito successivamente in ordine all'interno dello stesso passaggio di composizione) rispetto a dove è stato letto, costringendo Compose a ricomporre pianificando l'esecuzione di una fase o di un componibile precedente. Una scrittura all'indietro è un loop di ricomposizione non ottimizzato.
Conseguenze delle scritture all'indietro
Le scritture all'indietro non sono necessariamente un problema e non sempre causano arresti anomali, ma sono inefficienti e possono influire negativamente sulle prestazioni dell'app in diversi modi:
- Rendering di frame aggiuntivi e frame saltati: una scrittura inversa forza Compose a eseguire passaggi di composizione ridondanti su frame consecutivi, sprecando risorse CPU e GPU e potenzialmente causando jank.
- Problemi di correttezza del primo frame: se il componente richiede una scrittura all'indietro per risolvere le dimensioni o lo stato finali, il primo frame viene visualizzato con dati non validi, predefiniti o non impostati (ad esempio, dimensioni pari a zero o un offset non corretto). Ciò causa un effetto visivo di "pop" o un lampeggiamento del layout quando viene eseguito il rendering del secondo frame.
- Loop di ricomposizione infiniti: se una modifica dello stato altera le dimensioni del layout e le dimensioni del layout scrivono continuamente un nuovo valore nello stato, rischi di creare un loop di frame infinito in cui lo schermo ricompone costantemente ogni frame senza mai stabilizzarsi.
Flusso in avanti attraverso le fasi
Le modifiche di stato devono sempre procedere in avanti attraverso le fasi:
| Fase di lettura | Write Context | Accettabile? | Perché |
|---|---|---|---|
Layout (Modifier.offset { }) |
Composizione | Sì | La composizione aggiorna lo stato → Il layout lo legge in un secondo momento nello stesso frame senza ricomposizione. |
Pareggio (graphicsLayer { }, drawBehind { }) |
Composizione | Sì | La composizione aggiorna lo stato → Draw lo legge nella fase finale. Composizione e layout vengono ignorati completamente. |
| Disegna | Layout | Sì | Il layout aggiorna lo stato e il disegno lo legge, flusso valido. |
| Composizione | Callback dell'evento (onClick, onValueChange) che determina la modifica dello stato. Nota: i callback di layout non vengono conteggiati come eventi. |
Sì | Un evento modifica lo stato utilizzato per gestire la composizione. Se l'evento si verifica fuori dal frame (non in Composizione, Layout o Disegno), questo è valido. |
| Composizione | Coroutines (LaunchedEffect) |
Sì, con cautela | Aggiorna lo stato in modo asincrono in risposta al ciclo di vita/agli eventi. Le scritture dagli effetti possono essere valide, ma possono indicare una stratificazione dello stato inefficiente. Dovrebbero essere evitati, se possibile. |
| Posizionamento (nel layout) | Misura (nel layout) | Sì | In Layout, è accettabile aggiornare lo stato e poi leggerlo in Placement. |
| Misura (nel layout) | Placement (In Layout) | No - backwards write | La scrittura nello stato in un posizionamento che si trova più avanti nello stato di lettura causa un loop di nuova misurazione. |
| Composizione | Layout (onSizeChanged, LayoutModifier) |
No - Scrittura da destra a sinistra | Il layout invalida la composizione → Loop di ricomposizione. |
| Composizione | Pareggio (drawWithContent, Canvas) |
No - Scrittura al contrario | Il disegno invalida il loop Composizione → Ricomposizione. |
Combinazioni di fasi: indietro e avanti
Di seguito sono riportati esempi di scritture con effetto retroattivo in Compose e come puoi risolverli.
Al contrario: lettura in Composizione, scrittura in Layout
- Cosa succede: la composizione legge
componentHeightper determinare quale UI emettere. Più avanti nel frame, la fase di layout misura o posiziona le visualizzazioni e scrive un nuovo valore incomponentHeight(ad esempio, utilizzandoonSizeChanged,onGloballyPositionedoLayoutModifierpersonalizzato). - Risultato: la modifica di
componentHeightin Layout invalida la fase di composizione appena completata. Tieni presente cheonSizeChangedsegnala le dimensioni dopo il completamento della fase di misurazione del layout. Se il valore dello stato aggiornato si stabilizza al passaggio successivo, la ricomposizione potrebbe interrompersi dopo un frame aggiuntivo. Tuttavia, se il nuovo valore continua a modificare le dimensioni, si verifica un loop di frame infinito. Inoltre,onGloballyPositionedviene eseguito dopo il layout e il posizionamento, rendendo le scritture di stato al suo interno ancora più suscettibili a loop di ricomposizione e relayout continui tra frame consecutivi.
// ❌ 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) } } }
Al contrario: lettura in Composizione, scrittura in Disegno
- Cosa succede: lo stato viene letto nel corpo del componibile (fase di composizione),
ma modificato all'interno di
Modifier.drawWithContent,Modifier.drawBehindoCanvas(fase di disegno). - Risultato: la fase di disegno modifica lo stato → Composizione non valida → 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 } ) }
Al contrario: lettura nella composizione, scrittura nella composizione (stessa fase)
- Cosa succede: lettura di
countnella funzione componibile e modifica dicountdirettamente nello slot dei contenuti di un altro componibile dopo la lettura. - Risultato: il sistema di snapshot registra la lettura e la scrittura successiva all'interno della stessa fase di composizione, invalidando immediatamente l'ambito corrente.
// ❌ 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 }
Regole chiave per evitare scritture all'indietro
- Non scrivere nello stato in
onGloballyPositioned,onSizeChanged,onPlaced,onLayoutRectChangedoLayoutModifierse lo stato viene letto in Composition, in quanto ciò causa il problema di correttezza del primo frame.- Se le coordinate o le dimensioni del layout sono necessarie solo per il disegno personalizzato, leggile direttamente nella fase di disegno o di layout (ad esempio, utilizzando
Modifier.drawWithCacheoModifier.layout). - Per il dimensionamento a livello di finestra (
WindowWidthSizeClass): eleva l'osservazione delle dimensioni al livello della finestra. La composizione si dirama in base alle classi di dimensioni della finestra prima che venga eseguita la misurazione locale. - Mantieni uniforme la composizione: utilizza un singolo layout personalizzato o componenti come
FlowRowoLazyVerticalGridche regolano la misurazione e il posizionamento durante la Fase 2 senza ricomporre o alterare lo stato utilizzato nella composizione. - Utilizza la sottocomposizione: utilizza
BoxWithConstraintsoSubcomposeLayoutquando i componibili secondari devono essere ramificati in base alla larghezza o all'altezza locale. Fai attenzione: la sottocomposizione comporta un costo in termini di prestazioni e di solito può essere evitata. - Come ultima risorsa:consenti che il primo frame sia errato, memorizzando le dimensioni in
onSizeChangedper attivare una seconda ricomposizione del frame. Ciò causa un effetto di popping visibile del layout, jank e rischi di loop infiniti.
- Se le coordinate o le dimensioni del layout sono necessarie solo per il disegno personalizzato, leggile direttamente nella fase di disegno o di layout (ad esempio, utilizzando
- Non modificare lo stato dopo che è stato letto per la prima volta nella composizione:
- Sebbene sia possibile scrivere in sicurezza negli oggetti
MutableStatedurante la composizione al di fuori di unSideEffect, presta particolare attenzione a non scrivere in uno stato che potresti aver letto in precedenza nella composizione. Ti consigliamo di utilizzarerememberUpdatedStatequando si presenta la necessità di una scrittura dello stato nella composizione. La scrittura in uno stato durante la composizione in un altro modo è solitamente un segno di un effetto mancante o di uno stato o un composable progettato in modo inappropriato. Ricorda che la composizione è ottimistica e viene sempre eseguita con l'ultimo valore di uno stato, pertanto potresti non vedere tutte le modifiche di stato nelle ricomposizioni. Gli aggiornamenti della UI non devono essere utilizzati come un modo per gestire eventi una tantum, il che rende insolito che il valore di uno stato richieda l'aggiornamento a seguito della ricomposizione. - Evita di modificare gli stati osservati al di fuori della composizione (ad esempio, i campi
ViewModelo i flagisVisible). Le scritture di stato che influiscono sulla composizione appartengono alle lambda degli eventi (onClick), alle coroutine (LaunchedEffect) o agli effetti collaterali (SideEffect).rememberUpdatedStateè un'eccezione, in quanto è progettato per mutare lo stato utilizzato solo nel corpo di@Composable.
- Sebbene sia possibile scrivere in sicurezza negli oggetti
- Rimanda le letture dello stato alla fase più tardiva possibile:
- La lettura degli stati in Draw (
Modifier.graphicsLayer { alpha = ... }) o Layout (Modifier.offset { IntOffset(...) }) garantisce che le modifiche invalidino solo la Fase 2 o 3, saltando completamente la Fase 1 (Composizione).
- La lettura degli stati in Draw (