In questa pagina scoprirai il ciclo di vita di un composable e come Compose decide se un composable deve essere ricomposto.
Panoramica del ciclo di vita
Come indicato nella documentazione sulla gestione dello stato, una composizione descrive la UI della tua app e viene prodotta eseguendo i composable. Una composizione è una struttura ad albero dei composable che descrivono la tua UI.
Quando Jetpack Compose esegue i tuoi composable per la prima volta, durante la composizione iniziale, tiene traccia dei composable che chiami per descrivere la tua UI in una composizione. Poi, quando lo stato dell'app cambia, Jetpack Compose pianifica una ricomposizione. La ricomposizione si verifica quando Jetpack Compose riesegue i composable che potrebbero essere cambiati in risposta alle modifiche dello stato e poi aggiorna la composizione per riflettere le modifiche.
Una composizione può essere prodotta solo da una composizione iniziale e aggiornata tramite ricomposizione. L'unico modo per modificare una composizione è ricomporla.
Figura 1. Ciclo di vita di un componente componibile nella composizione. Entra nella composizione, viene ricomposto 0 o più volte e lascia la composizione.
La ricomposizione viene in genere attivata da una modifica a un oggetto
State<T>
. Compose
monitora questi elementi ed esegue tutti i composable nella composizione che leggono quel
particolare State<T>
, nonché tutti i composable che chiamano e che non possono essere
ignorati.
Se un composable viene chiamato più volte, nella composizione vengono inserite più istanze. Ogni chiamata ha il proprio ciclo di vita nella composizione.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
Figura 2. Rappresentazione di MyComposable
nella composizione. Se un
componente componibile viene chiamato più volte, nella
composizione vengono inserite più istanze. Un elemento di colore diverso indica che si tratta di un'istanza separata.
Anatomia di un elemento componibile in Composition
L'istanza di un elemento componibile in Composition è identificata dal relativo sito di chiamata. Il compilatore Compose considera ogni sito di chiamata come distinto. La chiamata di composable da più siti di chiamata creerà più istanze del composable nella composizione.
Se durante una ricomposizione un elemento componibile chiama elementi componibili diversi rispetto alla composizione precedente, Compose identificherà gli elementi componibili chiamati o non chiamati e, per gli elementi componibili chiamati in entrambe le composizioni, eviterà di ricomporli se i relativi input non sono cambiati.
Preservare l'identità è fondamentale per associare gli effetti collaterali al relativo componente, in modo che possano essere completati correttamente anziché riavviati per ogni ricomposizione.
Considera l'esempio seguente:
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
Nello snippet di codice riportato sopra, LoginScreen
chiamerà in modo condizionale il composable LoginError
e chiamerà sempre il composable LoginInput
. Ogni
chiamata ha un sito di chiamata e una posizione di origine unici, che il compilatore utilizzerà per
identificarla in modo univoco.
Figura 3. Rappresentazione di LoginScreen
nella composizione quando lo stato
cambia e si verifica una ricomposizione. Lo stesso colore indica che non è stato ricomposto.
Anche se LoginInput
è passato dall'essere chiamato per primo all'essere chiamato per secondo,
l'istanza LoginInput
verrà conservata durante le ricomposizioni. Inoltre,
poiché LoginInput
non ha parametri che sono cambiati durante
la ricomposizione, la chiamata a LoginInput
verrà ignorata da Compose.
Aggiungere informazioni aggiuntive per facilitare le ricomposizioni intelligenti
Se chiami un elemento componibile più volte, questo verrà aggiunto più volte anche a Composition. Quando un composable viene chiamato più volte dallo stesso sito di chiamata, Compose non dispone di informazioni per identificare in modo univoco ogni chiamata a quel composable, quindi l'ordine di esecuzione viene utilizzato in aggiunta al sito di chiamata per mantenere distinte le istanze. A volte questo comportamento è tutto ciò che serve, ma in alcuni casi può causare un comportamento indesiderato.
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
Nell'esempio precedente, Compose utilizza l'ordine di esecuzione oltre al sito di chiamata per mantenere l'istanza distinta nella composizione. Se un nuovo movie
viene aggiunto
alla fine dell'elenco, Compose può riutilizzare le istanze già presenti nella
composizione, poiché la loro posizione nell'elenco non è cambiata e, pertanto, l'input
movie
è lo stesso per queste istanze.
Figura 4. Rappresentazione di MoviesScreen
nella composizione quando un nuovo
elemento viene aggiunto in fondo all'elenco. I composable MovieOverview
nella
composizione possono essere riutilizzati. Lo stesso colore in MovieOverview
indica che il composable
non è stato ricomposto.
Tuttavia, se l'elenco movies
cambia aggiungendo elementi alla parte superiore o alla
parte centrale dell'elenco, rimuovendo o riordinando gli elementi, verrà eseguita una ricomposizione
in tutte le chiamate MovieOverview
il cui parametro di input ha cambiato posizione nell'elenco. Questo è estremamente importante se, ad esempio, MovieOverview
recupera
l'immagine di un film utilizzando un effetto collaterale. Se la ricomposizione avviene mentre l'effetto è in
corso, verrà annullata e ricomincerà.
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
Figura 5. Rappresentazione di MoviesScreen
nella composizione quando un nuovo
elemento viene aggiunto all'elenco. I composable MovieOverview
non possono essere riutilizzati e
tutti gli effetti collaterali verranno riavviati. Un colore diverso in MovieOverview
indica che il
componente è stato ricomposto.
Idealmente, vogliamo considerare l'identità dell'istanza MovieOverview
come
collegata all'identità di movie
che le viene trasmessa. Se riordiniamo l'elenco dei film, idealmente riordineremmo anche le istanze nell'albero di composizione anziché ricomporre ogni composizione MovieOverview
con un'istanza di film diversa. Compose ti consente di comunicare al runtime
quali valori vuoi utilizzare per identificare una determinata parte dell'albero: il
key
componente componibile.
Se racchiudi un blocco di codice con una chiamata al composable della chiave con uno o più
valori passati, questi valori verranno combinati per essere utilizzati per identificare l'istanza nella composizione. Il valore di un key
non deve essere univoco a livello globale, ma solo tra le invocazioni dei composable nel sito di chiamata. Quindi, in questo esempio, ogni movie
deve avere un
key
univoco tra i movies
; è accettabile se condivide key
con
un altro elemento componibile in un'altra parte dell'app.
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
Con quanto sopra, anche se gli elementi dell'elenco cambiano, Compose riconosce
le singole chiamate a MovieOverview
e può riutilizzarle.
Figura 6. Rappresentazione di MoviesScreen
nella composizione quando un nuovo
elemento viene aggiunto all'elenco. Poiché i composable MovieOverview
hanno chiavi uniche, Compose riconosce le istanze MovieOverview
che non sono cambiate e può riutilizzarle; i loro effetti collaterali continueranno a essere eseguiti.
Alcuni composable hanno il supporto integrato per il composable key
. Ad esempio,
LazyColumn
accetta la specifica di un key
personalizzato nel linguaggio DSL items
.
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
Ignorare se gli input non sono cambiati
Durante la ricomposizione, l'esecuzione di alcune funzioni componibili idonee può essere saltata completamente se i relativi input non sono cambiati rispetto alla composizione precedente.
Una funzione componibile è idonea per il salto a meno che:
- La funzione ha un tipo restituito diverso da
Unit
- La funzione è annotata con
@NonRestartableComposable
o@NonSkippableComposable
- Un parametro obbligatorio è di un tipo non stabile
Esiste una modalità di compilazione sperimentale, Strong Skipping, che allenta l'ultimo requisito.
Affinché un tipo sia considerato stabile, deve rispettare il seguente contratto:
- Il risultato di
equals
per due istanze sarà sempre lo stesso per le stesse due istanze. - Se una proprietà pubblica del tipo cambia, Composition riceverà una notifica.
- Anche tutti i tipi di proprietà pubbliche sono stabili.
Esistono alcuni tipi comuni importanti che rientrano in questo contratto che il
compilatore Compose tratterà come stabili, anche se non sono esplicitamente
contrassegnati come stabili utilizzando l'annotazione @Stable
:
- Tutti i tipi di valori primitivi:
Boolean
,Int
,Long
,Float
,Char
e così via. - Stringhe
- Tutti i tipi di funzioni (lambda)
Tutti questi tipi sono in grado di seguire il contratto di stable perché sono immutabili. Poiché i tipi immutabili non cambiano mai, non devono mai notificare la composizione della modifica, quindi è molto più facile rispettare questo contratto.
Un tipo notevole che è stabile ma è modificabile è il tipo MutableState
di Compose. Se un valore è contenuto in un MutableState
, l'oggetto stato complessivo è
considerato stabile perché Compose riceverà una notifica di eventuali modifiche alla
proprietà .value
di State
.
Quando tutti i tipi passati come parametri a un elemento componibile sono stabili, i valori dei parametri vengono confrontati per verificare l'uguaglianza in base alla posizione dell'elemento componibile nell'albero dell'interfaccia utente. La ricomposizione viene ignorata se tutti i valori sono invariati rispetto alla chiamata precedente.
Compose considera un tipo stabile solo se può dimostrarlo. Ad esempio, un'interfaccia è generalmente considerata non stabile e anche i tipi con proprietà pubbliche modificabili la cui implementazione potrebbe essere immutabile non sono stabili.
Se Compose non è in grado di dedurre che un tipo è stabile, ma vuoi forzare
Compose a considerarlo stabile, contrassegnalo con l'annotazione
@Stable
.
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
Nello snippet di codice riportato sopra, poiché UiState
è un'interfaccia, Compose potrebbe
normalmente considerare questo tipo come non stabile. Se aggiungi l'annotazione @Stable
, indichi a Compose che questo tipo è stabile, consentendo a Compose di favorire
ricomposizioni intelligenti. Ciò significa anche che Compose tratterà tutte le sue implementazioni come stabili se l'interfaccia viene utilizzata come tipo di parametro.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- State e Jetpack Compose
- Effetti collaterali in Compose
- Salvare lo stato dell'interfaccia utente in Compose