Scrittura all'indietro

Compose esegue un frame in tre fasi rigorosamente ordinate e in sequenza:

Tre fasi in sequenza: 1. Composizione, 2. Layout, 3. Disegno
Figura 1. Le tre fasi di un frame di Compose
  1. Composizione: esegue le funzioni @Composable per creare e aggiornare l'albero della UI.
  2. Layout: misura gli elementi secondari e poi li posiziona.
  3. 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.

Esempio di scrittura inversa attraverso le fasi del ciclo di ricomposizione
Figura 2. Esempio di scrittura inversa attraverso le fasi del loop di ricomposizione

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 componentHeight per determinare quale UI emettere. Più avanti nel frame, la fase di layout misura o posiziona le visualizzazioni e scrive un nuovo valore in componentHeight (ad esempio, utilizzando onSizeChanged, onGloballyPositioned o LayoutModifier personalizzato).
  • Risultato: la modifica di componentHeight in Layout invalida la fase di composizione appena completata. Tieni presente che onSizeChanged segnala 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, onGloballyPositioned viene 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.drawBehind o Canvas (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 count nella funzione componibile e modifica di count direttamente 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

  1. Non scrivere nello stato in onGloballyPositioned, onSizeChanged, onPlaced, onLayoutRectChanged o LayoutModifier se 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.drawWithCache o Modifier.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 FlowRow o LazyVerticalGrid che regolano la misurazione e il posizionamento durante la Fase 2 senza ricomporre o alterare lo stato utilizzato nella composizione.
    • Utilizza la sottocomposizione: utilizza BoxWithConstraints o SubcomposeLayout quando 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 onSizeChanged per attivare una seconda ricomposizione del frame. Ciò causa un effetto di popping visibile del layout, jank e rischi di loop infiniti.
  2. Non modificare lo stato dopo che è stato letto per la prima volta nella composizione:
    • Sebbene sia possibile scrivere in sicurezza negli oggetti MutableState durante la composizione al di fuori di un SideEffect, presta particolare attenzione a non scrivere in uno stato che potresti aver letto in precedenza nella composizione. Ti consigliamo di utilizzare rememberUpdatedState quando 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 ViewModel o i flag isVisible). 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.
  3. 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).