Compose esegue un frame in tre fasi strettamente ordinate e in sequenza:
- Composizione: esegue
@Composablefunzioni per creare e aggiornare l'albero dell'interfaccia utente. - Layout: misura i bambini e poi li posiziona.
- Disegno: invia comandi di disegno del canvas per visualizzare i 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.
Che cosa rende una scrittura "inversa"?
Una scrittura all'indietro si verifica ogni volta che uno stato viene modificato in una fase successiva (o in un ambito downstream, ovvero un ambito componibile eseguito in un secondo momento nell'ordine all'interno della stessa passata di composizione) rispetto a dove è stato letto, costringendo Compose a ricomporsi pianificando l'esecuzione di una fase o di un elemento componibile precedente. Una scrittura all'indietro è un ciclo di ricomposizione non ottimizzato.
Conseguenze delle scritture all'indietro
Le scritture all'indietro non sono necessariamente un male e non sempre causano arresti anomali, ma sono inefficienti e possono danneggiare le prestazioni dell'app in diversi modi:
- Rendering di frame aggiuntivi e frame interrotti: una scrittura all'indietro 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 fotogramma: se il componente richiede una scrittura all'indietro per risolvere le dimensioni o lo stato finali, il primo fotogramma viene visualizzato con dati non validi, predefiniti o non definitivi (ad esempio dimensioni pari a zero o un offset errato). Ciò causa un'interruzione visiva o un lampeggio del layout quando viene visualizzato il secondo fotogramma.
- Loop di ricomposizione infiniti: se una modifica dello stato altera il dimensionamento del layout e il dimensionamento del layout scrive continuamente un nuovo valore nello stato, puoi rischiare di creare un loop di frame infinito in cui lo schermo si ricompone costantemente a ogni frame senza mai stabilizzarsi.
Avanzare nel flusso attraverso le fasi
Le modifiche dello 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 ricomporlo. |
Estrazione (graphicsLayer { }, drawBehind { }) |
Composizione | Sì | La composizione aggiorna lo stato → Draw lo legge nella fase finale. Composizione e layout vengono ignorati completamente. |
| Disegna | Layout | Sì | Layout updates the state, and draw reads it, valid flow. |
| Composizione | Callback evento (onClick, onValueChange) che determina la modifica dello stato di guida. Nota: i callback del layout non vengono conteggiati come eventi. |
Sì | Un evento modifica lo stato utilizzato per guidare la composizione. Se l'evento si verifica fuori dal frame (non in Composizione, Layout o Disegno), è valido. |
| Composizione | Coroutine (LaunchedEffect) |
Sì, con cautela | Aggiorna in modo asincrono lo stato in risposta al ciclo di vita/agli eventi. Le scritture dagli effetti possono essere valide, ma possono indicare una stratificazione inefficiente dello stato. Devono essere evitati, se possibile. |
| Posizionamento (nel layout) | Misura (nel layout) | Sì | In Layout, l'aggiornamento dello stato e la lettura dello stato nel posizionamento sono accettabili. |
| Misura (nel layout) | Posizionamento (nel layout) | No - backwards write | La scrittura nello stato di posizionamento che poi si trova più avanti nello stato di lettura causa un ciclo di ri-misurazione. |
| Composizione | Layout (onSizeChanged, LayoutModifier) |
No - Scrittura all'indietro | Il layout invalida la composizione → ciclo di ricomposizione. |
| Composizione | Estrazione (drawWithContent, Canvas) |
No - Backwards write | Il disegno invalida la composizione → ciclo di ricomposizione. |
Combinazioni di fasi: indietro e avanti
Di seguito sono riportati esempi di scrittura all'indietro in Compose e come risolverli.
Da destra a sinistra: lettura nella composizione, scrittura nel 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
componentHeightnel layout invalida la fase di composizione appena completata. Tieni presente cheonSizeChangedgenera report sulle dimensioni dopo il completamento del passaggio 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 ciclo di frame infinito. Inoltre,onGloballyPositionedviene eseguito dopo il layout e il posizionamento, rendendo le scritture di stato al suo interno ancora più suscettibili a ricomposizioni e riposizionamenti continui nei 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) } } }
Indietro: lettura in Composizione, scrittura in Disegno
- Cosa succede: lo stato viene letto nel corpo del composable (fase di composizione),
ma mutato all'interno di
Modifier.drawWithContent,Modifier.drawBehindoCanvas(fase di disegno). - Risultato: la fase di disegno modifica lo stato → la composizione viene invalidata → ciclo 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 } ) }
Indietro: lettura nella composizione, scrittura nella composizione (stessa fase)
- Cosa succede: lettura di
countin una funzione componibile e modifica dicountdirettamente in un altro slot di contenuti componibili dopo la lettura. - Risultato: il sistema di snapshot registra la lettura e la scrittura successiva all'interno della stessa passata 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 impedire le scritture all'indietro
- Non scrivere nello stato
onGloballyPositioned,onSizeChangedoLayoutModifierse lo stato viene letto in Composizione, 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 layout (ad esempio, utilizzando
Modifier.drawWithCacheoModifier.layout). - Per il dimensionamento a livello di finestra (
WindowWidthSizeClass): osserva le dimensioni del paranco a livello di finestra. I rami di composizione nelle classi di dimensioni della finestra prima che venga eseguita la misurazione locale. - Mantieni uniforme la composizione:utilizza un unico 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 composizione secondaria:utilizza
BoxWithConstraintsoSubcomposeLayoutquando i componenti combinabili secondari devono ramificarsi in base alla larghezza o all'altezza locale. Usa con cautela: la composizione secondaria comporta un costo in termini di prestazioni e in genere può essere evitata. - Come ultima risorsa: consenti al primo frame di essere errato, memorizzando le dimensioni in
onSizeChangedper attivare una seconda ricomposizione del frame. Ciò causa sfarfallio visibile del layout, scatti 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 layout (ad esempio, utilizzando
- Non modificare lo stato dopo la prima lettura nella composizione:
- Anche se puoi scrivere in modo sicuro negli oggetti
MutableStatedurante la composizione al di fuori di unSideEffect, presta particolare attenzione per assicurarti di non scrivere in uno stato che potresti aver letto in precedenza nella composizione. Ti consigliamo di utilizzarerememberUpdatedStatequando si presenta la necessità di scrivere uno stato nella composizione. Scrivere a uno stato durante la composizione in un altro modo è solitamente un segno di un effetto mancante o di uno stato o di un elemento componibile progettato in modo inappropriato. Ricorda che la composizione è ottimistica e viene sempre eseguita con l'ultimo valore di uno stato, quindi potresti non visualizzare tutte le modifiche dello stato nelle ricomposizioni. Gli aggiornamenti dell'interfaccia utente non devono essere utilizzati per gestire eventi una tantum, il che rende insolito che il valore di uno stato richieda di essere aggiornato a seguito della ricomposizione. - Evita di modificare gli stati osservati al di fuori della composizione (ad esempio, campi
ViewModelo flagisVisible). State scrive che affect composition appartengono a espressioni lambda di eventi (onClick), coroutine (LaunchedEffect) o effetti collaterali (SideEffect).rememberUpdatedStateè un'eccezione in quanto è progettato per modificare lo stato utilizzato solo nel corpo di@Composable.
- Anche se puoi scrivere in modo sicuro negli oggetti
- Differisci le letture dello stato alla fase più recente possibile:
- Gli stati di lettura in Disegno (
Modifier.graphicsLayer { alpha = ... }) o Layout (Modifier.offset { IntOffset(...) }) assicurano che le modifiche invalidino solo la fase 2 o 3, saltando completamente la fase 1 (composizione).
- Gli stati di lettura in Disegno (