In questa pagina scoprirai il ciclo di vita di un componibile e in che modo Compose decide se deve essere ricomposto.
Panoramica del ciclo di vita
Come menzionato nella documentazione sulla gestione dello stato, una composizione descrive l'interfaccia utente della tua app ed è prodotta eseguendo componibili. Una composizione è una struttura ad albero dei componibili che descrivono l'interfaccia utente.
Quando Jetpack Compose esegue i componibili per la prima volta, durante la composizione iniziale, tiene traccia degli elementi componibili che chiami per descrivere l'interfaccia utente in una composizione. Quando lo stato dell'app cambia, Jetpack Compose pianifica una ricomposizione. La ricomposizione avviene quando Jetpack Compose esegue i componibili che potrebbero essere stati modificati in risposta ai cambiamenti di stato e aggiorna la composizione per riflettere eventuali modifiche.
Una composizione può essere prodotta solo da una composizione iniziale e aggiornata tramite ricomposizione. L'unico modo per modificare una composizione è tramite la ricomposizione.
Figura 1. Ciclo di vita di un componibile nella composizione. Entra nella composizione, viene ricomposta 0 o più volte e poi esce dalla composizione.
In genere, la ricomposizione viene attivata da una modifica a un oggetto State<T>
. Compose tiene traccia di questi elementi ed esegue tutti i componibili nella composizione che leggono quel particolare State<T>
, oltre agli eventuali componibili chiamati che non possono essere saltati.
Se un componibile viene chiamato più volte, più istanze vengono inserite nella composizione. 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 componibile viene richiamato più volte, più istanze vengono inserite nella composizione. Un elemento con un colore diverso indica che si tratta di un'istanza separata.
Anatomia di un componibile in composizione
L'istanza di un componibile in Composizione viene identificata dal relativo sito di chiamata. Il compilatore Compose considera ogni sito di chiamata come distinto. La chiamata degli elementi componibili da più siti di chiamata creerà più istanze dell'elemento componibile in Composizione.
Se durante una ricomposizione un componibile chiama componenti componibili diversi rispetto alla composizione precedente, Compose identificherà quali elementi componibili sono stati chiamati o meno e, per quelli che sono stati chiamati in entrambe le composizioni, evita di ricomporli se i loro input non sono cambiati.
Conservare l'identità è fondamentale per associare gli effetti collaterali al componente componibile, in modo che possa essere completato con successo anziché ripartire 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 componibile
LoginError
e chiamerà sempre il componibile LoginInput
. Ogni chiamata ha un sito di chiamata e una posizione di origine univoci, 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 è stata ricomposta.
Anche se LoginInput
è passata da essere chiamata prima a essere chiamata seconda,
l'istanza LoginInput
verrà conservata in tutte le ricomposizioni. Inoltre,
poiché LoginInput
non ha parametri cambiati
nella ricomposizione, la chiamata a LoginInput
verrà ignorata da Compose.
Aggiungi informazioni aggiuntive per favorire le ricomposizioni intelligenti
Richiamare un componibile più volte significa aggiungerlo alla composizione più volte. Quando chiami un componibile più volte dallo stesso sito di chiamata, Compose non dispone di informazioni che consentano di identificare in modo univoco ogni chiamata al componibile. Di conseguenza, oltre al sito della chiamata, viene utilizzato l'ordine di esecuzione oltre al sito della chiamata per mantenere distinte le istanze. Questo comportamento a volte è sufficiente, ma in alcuni casi può causare comportamenti indesiderati.
@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 della chiamata per mantenere l'istanza distinta nella Composizione. Se viene aggiunto un nuovo movie
in parte inferiore dell'elenco, Compose può riutilizzare le istanze già presenti nella composizione poiché la loro posizione nell'elenco non è cambiata e, di conseguenza, 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 componibili MovieOverview
nella
Composizione possono essere riutilizzati. Lo stesso colore in MovieOverview
indica che il componibile
non è stato ricomposto.
Tuttavia, se l'elenco movies
cambia in seguito all'aggiunta in top o al centro dell'elenco, alla rimozione o al riordinamento degli elementi, si verificherà una ricomposizione in tutte le chiamate MovieOverview
il cui parametro di input è cambiato posizione nell'elenco. Questo è estremamente importante, ad esempio, MovieOverview
recupera
l'immagine di un filmato utilizzando un effetto collaterale. Se la ricomposizione avviene mentre l'effetto è in corso, viene annullato e verrà riavviato.
@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 viene aggiunto
un nuovo elemento all'elenco. I componenti componibili MovieOverview
non possono essere riutilizzati e
tutti gli effetti collaterali verranno riavviati. Se un colore è diverso in MovieOverview
,
il componibile è stato ricomposto.
Idealmente, vogliamo considerare l'identità dell'istanza MovieOverview
come
collegata all'identità dell'elemento movie
che le viene passata. Se riordinassimo l'elenco dei filmati, l'ideale sarebbe riordinare le istanze nell'albero della composizione, invece di ricomporre ogni componibile MovieOverview
con un'istanza di filmato diversa. Compose consente di indicare al runtime i valori da utilizzare per identificare una determinata parte dell'albero: l'elemento componibile key
.
Eseguendo l'avvolgimento di un blocco di codice con una chiamata alla chiave componibile con uno o più valori trasferiti, questi valori verranno combinati per essere utilizzati per identificare l'istanza nella composizione. Il valore di key
non deve essere necessariamente
a livello globale, ma deve essere univoco solo tra le chiamate degli
componibili al sito di chiamata. Quindi, in questo esempio, ogni movie
deve avere un
key
unico tra tutti movies
; va bene se lo condivide key
con
altri componibili altrove nell'app.
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
Con quanto descritto sopra, anche se gli elementi nell'elenco cambiano, Compose riconosce
le singole chiamate a MovieOverview
e può riutilizzarle.
Figura 6. Rappresentazione di MoviesScreen
nella composizione quando viene aggiunto
un nuovo elemento all'elenco. Poiché gli elementi componibili MovieOverview
hanno chiavi univoche, Compose riconosce le istanze MovieOverview
non modificate e può riutilizzarle; gli effetti collaterali continueranno a essere eseguiti.
Alcuni componibili hanno il supporto integrato per l'elemento componibile key
. Ad esempio, LazyColumn
accetta di specificare un valore key
personalizzato in DSL items
.
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
Verrà ignorato se gli input non sono stati modificati
Durante la ricomposizione, l'esecuzione di alcune funzioni componibili idonee può essere saltata completamente se gli input non sono cambiati rispetto alla composizione precedente.
Una funzione componibile può essere saltata a meno che:
- La funzione ha un tipo restituito non
Unit
- La funzione è annotata con
@NonRestartableComposable
o@NonSkippableComposable
- Un parametro obbligatorio è di tipo non stabile
Esiste una modalità di compilazione sperimentale, Strong Skipping, che allenta l'ultimo requisito.
Affinché un tipo sia considerato stabile, deve essere conforme al seguente contratto:
- Il risultato di
equals
per due istanze sarà per sempre lo stesso per le stesse due istanze. - Se una proprietà pubblica di questo tipo cambia, la composizione riceve una notifica.
- Anche tutti i tipi di proprietà pubbliche sono stabili.
Esistono alcuni tipi comuni importanti che rientrano in questo contratto e che il compilatore di Compose tratterà come stabili, anche se non sono esplicitamente contrassegnati come stabili tramite l'annotazione @Stable
:
- Tutti i tipi di valori primitivi:
Boolean
,Int
,Long
,Float
,Char
e così via. - Stringhe
- Tutti i tipi di funzione (lambda)
Tutti questi tipi sono in grado di seguire il contratto di stabile perché sono immutabili. Poiché i tipi immutabili non cambiano mai, non è necessario inviare una notifica alla composizione della modifica, quindi è molto più facile seguire questo contratto.
Un tipo noto che è stabile ma è modificabile è il tipo MutableState
di Compose. Se un valore viene mantenuto in un MutableState
, l'oggetto di stato nel complesso viene considerato stabile poiché Compose riceverà una notifica di eventuali modifiche alla proprietà .value
di State
.
Quando tutti i tipi passati come parametri a un componibile sono stabili, i valori dei parametri vengono confrontati per ottenere l'uguaglianza in base alla posizione componibile nella struttura ad albero dell'interfaccia utente. La ricomposizione viene saltata se tutti i valori non sono stati modificati dalla chiamata precedente.
Compose considera un tipo stabile solo se può dimostrarlo. Ad esempio, un'interfaccia in genere viene considerata non stabile e anche i tipi con proprietà pubbliche modificabili la cui implementazione potrebbe essere immutabile non lo sono.
Se Compose non è in grado di dedurre che un tipo è stabile, ma vuoi forzare
Compose a trattarlo come 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 generalmente considerare questo tipo come non stabile. Se aggiungi l'annotazione @Stable
, indichi a Compose che questo tipo è stabile, consentendo a Compose di favorire le ricomposizioni intelligenti. Ciò significa anche che Compose tratterà tutte le sue
implementazioni come stabili se l'interfaccia viene utilizzata come tipo di parametro.
Consigliato 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