Come la maggior parte degli altri toolkit UI, Compose esegue il rendering di un frame in diverse fasi distinte. Ad esempio, il sistema di visualizzazione Android ha tre fasi principali: misurazione, layout e disegno. Compose è molto simile, ma all'inizio ha un'importante fase aggiuntiva chiamata composizione.
La documentazione di Compose descrive la composizione in Thinking in Compose e State and Jetpack Compose.
Le tre fasi di un frame
Compose ha tre fasi principali:
- Composizione: cosa mostrare nell'interfaccia utente. Compose esegue funzioni componibili e crea una descrizione della tua UI.
- Layout: dove posizionare l'interfaccia utente. Questa fase è costituita da due passaggi: misurazione e posizionamento. Gli elementi di layout si misurano e si posizionano, insieme a eventuali elementi secondari, in coordinate 2D, per ogni nodo dell'albero del layout.
- Disegno: come viene eseguito il rendering. Gli elementi dell'interfaccia utente vengono disegnati in un canvas, di solito uno schermo del dispositivo.

L'ordine di queste fasi è generalmente lo stesso, consentendo ai dati di fluire in una
direzione dalla composizione al layout al disegno per produrre un frame (noto anche
come flusso di dati unidirezionale). BoxWithConstraints
, LazyColumn
e
LazyRow
sono eccezioni notevoli, in cui la composizione dei relativi elementi secondari
dipende dalla fase di layout dell'elemento principale.
A livello concettuale, ciascuna di queste fasi si verifica per ogni frame. Tuttavia, per ottimizzare le prestazioni, Compose evita di ripetere il lavoro che calcolerebbe gli stessi risultati dagli stessi input in tutte queste fasi. Compose salta l'esecuzione di una funzione componibile se può riutilizzare un risultato precedente e Compose UI non riorganizza o ridisegna l'intero albero se non è necessario. Compose esegue solo la quantità minima di lavoro necessaria per aggiornare la UI. Questa ottimizzazione è possibile perché Compose tiene traccia delle letture dello stato nelle diverse fasi.
Comprendere le fasi
Questa sezione descrive in modo più dettagliato come vengono eseguite le tre fasi di Compose per i componenti componibili.
Composizione
Nella fase di composizione, il runtime di Compose esegue funzioni componibili e genera una struttura ad albero che rappresenta la tua UI. Questo albero dell'interfaccia utente è costituito da nodi di layout che contengono tutte le informazioni necessarie per le fasi successive, come mostrato nel seguente video:
Figura 2. L'albero che rappresenta la tua UI creato nella fase di composizione.
Una sottosezione dell'albero del codice e della UI ha il seguente aspetto:

In questi esempi, ogni funzione componibile nel codice corrisponde a un singolo nodo di layout nell'albero della UI. In esempi più complessi, i composable possono contenere logica e flusso di controllo e produrre un albero diverso in base a stati diversi.
Layout
Nella fase di layout, Compose utilizza l'albero dell'interfaccia utente prodotto nella fase di composizione come input. La raccolta di nodi di layout contiene tutte le informazioni necessarie per decidere le dimensioni e la posizione di ogni nodo nello spazio 2D.
Figura 4. La misurazione e il posizionamento di ogni nodo del layout nell'albero della UI durante la fase di layout.
Durante la fase di layout, l'albero viene attraversato utilizzando il seguente algoritmo in tre passaggi:
- Misura i figli: un nodo misura i suoi figli, se presenti.
- Decide own size: in base a queste misurazioni, un nodo decide le proprie dimensioni.
- Posiziona figli: ogni nodo figlio viene posizionato rispetto alla posizione del nodo stesso.
Al termine di questa fase, ogni nodo di layout ha:
- Larghezza e altezza assegnate
- Una coordinata x, y in cui deve essere disegnato
Ricorda l'albero dell'interfaccia utente della sezione precedente:
Per questo albero, l'algoritmo funziona nel seguente modo:
- Il
Row
misura i suoi elementi secondari,Image
eColumn
. - Viene misurato il
Image
. Non ha elementi secondari, quindi decide le proprie dimensioni e le comunica all'Row
. - Successivamente viene misurato il
Column
. Misura prima i propri figli (due composableText
). - Viene misurato il primo
Text
. Non ha elementi secondari, quindi decide le proprie dimensioni e le comunica aColumn
.- Il secondo
Text
viene misurato. Non ha elementi secondari, quindi decide le proprie dimensioni e le comunica aColumn
.
- Il secondo
- Il
Column
utilizza le misure del bambino per decidere la propria taglia. Utilizza la larghezza massima del figlio e la somma dell'altezza dei suoi figli. Column
posiziona i suoi elementi secondari in relazione a se stesso, mettendoli uno sotto l'altro verticalmente.- Il
Row
utilizza le misure del bambino per decidere la propria taglia. Utilizza l'altezza massima del figlio e la somma delle larghezze dei suoi figli. poi posiziona i relativi figli.
Tieni presente che ogni nodo è stato visitato una sola volta. Il runtime di Compose richiede un solo passaggio nell'albero della UI per misurare e posizionare tutti i nodi, il che migliora il rendimento. Quando il numero di nodi nell'albero aumenta, il tempo impiegato per attraversarlo aumenta in modo lineare. Al contrario, se ogni nodo viene visitato più volte, il tempo di attraversamento aumenta in modo esponenziale.
Disegno
Nella fase di disegno, l'albero viene attraversato di nuovo dall'alto verso il basso e ogni nodo si disegna a turno sullo schermo.
Figura 5. La fase di disegno disegna i pixel sullo schermo.
Utilizzando l'esempio precedente, i contenuti dell'albero vengono disegnati nel seguente modo:
- Il
Row
disegna tutti i contenuti che potrebbe avere, ad esempio un colore di sfondo. - Il
Image
si disegna da solo. - Il
Column
si disegna da solo. - Il primo e il secondo
Text
vengono disegnati rispettivamente.
Figura 6. Un albero della UI e la relativa rappresentazione disegnata.
Letture dello stato
Quando leggi il value
di un snapshot state
durante una delle fasi
elencate in precedenza, Compose tiene traccia automaticamente di ciò che stava facendo quando ha letto
il value
. Questo monitoraggio consente a Compose di eseguire nuovamente il lettore quando
cambia lo stato di value
ed è la base dell'osservabilità dello stato in Compose.
In genere si crea lo stato utilizzando mutableStateOf()
e poi si accede tramite
uno dei due modi: accedendo direttamente alla proprietà value
o, in alternativa, utilizzando
un delegato di proprietà Kotlin. Puoi scoprire di più in Stato nei
composables. Ai fini di questa guida, per "lettura dello stato" si intende uno dei due metodi di accesso equivalenti.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
Sotto il cofano del delegato di proprietà, le funzioni "getter" e "setter"
vengono utilizzate per accedere e aggiornare value
di State. Queste funzioni getter e setter vengono richiamate solo quando fai riferimento alla proprietà come valore e non quando viene creata, motivo per cui i due modi descritti in precedenza sono equivalenti.
Ogni blocco di codice che può essere eseguito nuovamente quando cambia uno stato di lettura è un
ambito di riavvio. Compose tiene traccia delle modifiche dello stato value
e riavvia gli ambiti
in fasi diverse.
Letture dello stato in fasi
Come accennato in precedenza, in Compose ci sono tre fasi principali e Compose monitora lo stato di lettura di ciascuna. In questo modo, Compose può notificare solo le fasi specifiche che devono eseguire il lavoro per ogni elemento interessato della tua UI.
Le sezioni seguenti descrivono ogni fase e cosa succede quando viene letto un valore di stato al suo interno.
Fase 1: composizione
Le letture dello stato all'interno di una funzione @Composable
o di un blocco lambda influiscono sulla composizione
e potenzialmente sulle fasi successive. Quando lo stato di value
cambia, il
ricompositore pianifica le ripetizioni di tutte le funzioni componibili che leggono value
dello stato. Tieni presente che il runtime potrebbe decidere di ignorare alcune o tutte le
funzioni componibili se gli input non sono cambiati. Per saperne di più, consulta Ignorare se gli input
non sono stati modificati.
A seconda del risultato della composizione, l'UI Compose esegue le fasi di layout e disegno. Potrebbe saltare queste fasi se i contenuti rimangono invariati e le dimensioni e il layout non cambiano.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Fase 2: layout
La fase di layout è costituita da due passaggi: misurazione e posizionamento. Il passaggio di misurazione esegue la lambda di misurazione passata al composable Layout
, al metodo MeasureScope.measure
dell'interfaccia LayoutModifier
e ad altri.
Il passaggio di posizionamento esegue il blocco di posizionamento della funzione layout
, il blocco lambda di Modifier.offset { … }
e funzioni simili.
La lettura dello stato durante ciascuno di questi passaggi influisce sul layout e potenzialmente sulla
fase di disegno. Quando lo stato di value
cambia, l'interfaccia utente Compose pianifica la fase di layout. Esegue anche la fase di disegno se le dimensioni o la posizione sono cambiate.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Fase 3: disegno
Le letture dello stato durante il codice di disegno influiscono sulla fase di disegno. Esempi comuni
includono Canvas()
, Modifier.drawBehind
e Modifier.drawWithContent
. Quando
lo stato di value
cambia, Compose UI esegue solo la fase di disegno.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Letture dello stato di Optimize
Poiché Compose esegue il monitoraggio della lettura dello stato localizzato, puoi ridurre al minimo la quantità di lavoro eseguita leggendo ogni stato in una fase appropriata.
Considera l'esempio seguente. Questo esempio ha un Image()
che utilizza il modificatore di offset per compensare la posizione del layout finale, ottenendo un effetto di parallasse durante lo scorrimento dell'utente.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Questo codice funziona, ma comporta un rendimento non ottimale. Come scritto, il codice
legge value
dello stato firstVisibleItemScrollOffset
e lo passa alla
funzione Modifier.offset(offset: Dp)
. Man mano che l'utente scorre, il
firstVisibleItemScrollOffset
del value
cambierà. Come hai imparato, Compose
monitora tutte le letture dello stato in modo da poter riavviare (richiamare) il codice di lettura,
che in questo esempio è il contenuto di Box
.
Questo è un esempio di lettura di uno stato nella fase di composizione. Non è necessariamente una cosa negativa, anzi è la base della ricomposizione, che consente alle modifiche dei dati di generare una nuova UI.
Punto chiave: questo esempio non è ottimale perché ogni evento di scorrimento comporta la rivalutazione, la misurazione, la disposizione e infine il disegno dell'intero contenuto componibile. Attivi la fase di composizione a ogni scorrimento anche se i contenuti visualizzati non sono cambiati, ma solo la loro posizione. Puoi ottimizzare la lettura dello stato per attivare nuovamente solo la fase di layout.
Offset con lambda
È disponibile un'altra versione del modificatore di offset:
Modifier.offset(offset: Density.() -> IntOffset)
.
Questa versione accetta un parametro lambda, in cui l'offset risultante viene restituito dal blocco lambda. Aggiorna il codice per utilizzarlo:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Perché questo metodo è più efficiente? Il blocco lambda che fornisci al modificatore viene
richiamato durante la fase di layout (in particolare, durante il passaggio di posizionamento
della fase di layout), il che significa che lo stato di firstVisibleItemScrollOffset
non
viene più letto durante la composizione. Poiché Compose tiene traccia di quando viene letto lo stato,
questa modifica significa che se value
di firstVisibleItemScrollOffset
cambia,
Compose deve solo riavviare le fasi di layout e disegno.
Naturalmente, spesso è assolutamente necessario leggere gli stati nella fase di composizione. Tuttavia, in alcuni casi puoi ridurre al minimo il numero di
ricomposizioni filtrando le modifiche dello stato. Per saperne di più,
vedi derivedStateOf
: convertire uno o più oggetti di stato in un altro
stato.
Ciclo di ricomposizione (dipendenza ciclica dalle fasi)
In precedenza, questa guida menzionava che le fasi di Compose vengono sempre richiamate nello stesso ordine e che non è possibile tornare indietro nello stesso frame. Tuttavia, ciò non impedisce alle app di entrare in cicli di composizione in frame diversi. Considera questo esempio:
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
Questo esempio implementa una colonna verticale, con l'immagine in alto e il testo sotto. Utilizza Modifier.onSizeChanged()
per ottenere le dimensioni risolte
dell'immagine, quindi utilizza Modifier.padding()
sul testo per spostarlo verso il basso.
La conversione innaturale da Px
a Dp
indica già che il codice presenta un problema.
Il problema di questo esempio è che il codice non arriva al layout "finale " all'interno di un singolo frame. Il codice si basa su più frame, che esegue un lavoro non necessario e fa sì che l'interfaccia utente salti sullo schermo per l'utente.
Composizione del primo frame
Durante la fase di composizione del primo frame, imageHeightPx
è inizialmente
0
. Di conseguenza, il codice fornisce il testo con Modifier.padding(top = 0)
.
La fase di layout successiva richiama il callback del modificatore onSizeChanged
,
che aggiorna imageHeightPx
all'altezza effettiva dell'immagine. Componi, poi
pianifica una ricomposizione per il frame successivo. Tuttavia, durante la fase di estrazione attuale, il testo viene visualizzato con un padding di 0
, poiché il valore imageHeightPx
aggiornato non è ancora stato applicato.
Composizione del secondo frame
Compose avvia il secondo frame, attivato dalla modifica del valore di imageHeightPx
. Nella fase di composizione di questo frame, lo stato viene letto all'interno del blocco di contenuti Box
. Il testo ora viene fornito con un riempimento che corrisponde con precisione all'altezza dell'immagine. Durante la fase di layout, imageHeightPx
viene impostato di nuovo; tuttavia,
non viene pianificata alcuna ricomposizione perché il valore rimane coerente.
Questo esempio può sembrare forzato, ma fai attenzione a questo pattern generale:
Modifier.onSizeChanged()
,onGloballyPositioned()
o altre operazioni di layout- Aggiorna uno stato
- Utilizza questo stato come input per un modificatore del layout (
padding()
,height()
o simili) - Potenziale ripetizione
La correzione per l'esempio precedente consiste nell'utilizzare le primitive di layout corrette. L'esempio
precedente può essere implementato con un Column()
, ma potresti avere un esempio più
complesso che richiede qualcosa di personalizzato, il che richiederà la scrittura di un
layout personalizzato. Per ulteriori informazioni, consulta la guida Layout personalizzati.
Il principio generale è quello di avere un'unica fonte di dati per più elementi dell'interfaccia utente che devono essere misurati e posizionati l'uno rispetto all'altro. L'utilizzo di un elemento primitivo di layout appropriato o la creazione di un layout personalizzato significa che l'elemento padre condiviso minimo funge da fonte attendibile che può coordinare la relazione tra più elementi. L'introduzione di uno stato dinamico viola questo principio.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- State e Jetpack Compose
- Elenchi e griglie
- Kotlin per Jetpack Compose