Ciclo di vita dei componibili

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 l'interfaccia utente dell'app e viene prodotta dall'esecuzione dei composabili. Una composizione è una struttura ad albero dei componibili che descrivono l'interfaccia utente.

Quando Jetpack Compose esegue i tuoi composabili per la prima volta, durante la composizione iniziale, tiene traccia dei composabili che chiami per descrivere la tua UI in una composizione. Quando lo stato dell'app cambia, Jetpack compose pianifica una ricompozione. La ricompozione si verifica quando Jetpack Compose riesegue i composabili che potrebbero essere cambiati in risposta alle modifiche dello stato, e poi aggiorna la composizione in base alle eventuali modifiche.

Una composizione può essere prodotta solo da una composizione iniziale e aggiornata tramite la ricomposizio. L'unico modo per modificare una composizione è tramite la ricostituzione.

Diagramma che mostra il ciclo di vita di un composable

Figura 1. Ciclo di vita di un componibile nella composizione. Entra nella composizione, viene ricomposto zero o più volte e lascia la composizione.

La ricomposizione viene in genere attivata da una modifica a un oggetto State<T>. Compose li monitora ed esegue tutti i composable nella composizione che leggono quel State<T> specifico e tutti i composable chiamati che non possono essere scoltati.

Se un componibile 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")
    }
}

Diagramma che mostra la disposizione gerarchica degli elementi nello snippet di codice precedente

Figura 2. Rappresentazione di MyComposable nella composizione. Se un composable viene chiamato più volte, nella composizione vengono inserite più istanze. Un elemento con un colore diverso indica che si tratta di un'istanza distinta.

Anatomia di un composable in Composizione

L'istanza di un composable in Composition è identificata dal relativo sito di chiamata. Il compilatore Compose considera ogni sito di chiamata distinto. Se si richiamano elementi componibili da più siti di chiamata, verranno create più istanze del componibile in Compose.

Se durante una ricompozione un composable chiama composable diversi rispetto a quelli chiamati durante la composizione precedente, Compose identificherà i composable che sono stati chiamati o meno e per i composable chiamati in entrambe le composizioni, Compose eviterà di ricomporli se i relativi input non sono cambiati.

La conservazione dell'identità è fondamentale per associare gli effetti collaterali al relativo composable, in modo che possano essere completati correttamente anziché riavviare per ogni ricompozione.

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à condizionatamente il composable LoginError e chiamerà sempre il composable LoginInput. Ogni chiamata ha un sito di chiamata e una posizione di origine univoci, che il compilatore utilizzerà per identificarla in modo univoco.

Diagramma che mostra come viene ricomposto il codice precedente se il flag showError viene modificato in true. Viene aggiunto il componibile LoginError, ma gli altri componibili non vengono ricomposti.

Figura 3. Rappresentazione di LoginScreen nella composizione quando lo stato cambia e si verifica una ricostituzione. 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 ricostruzioni. Inoltre, poiché LoginInput non ha parametri che sono stati modificati durante la ricompozione, la chiamata a LoginInput verrà saltata da Compose.

Aggiungere informazioni aggiuntive per facilitare le ricostruzioni intelligenti

Se chiami un composable più volte, lo aggiungerai alla composizione più volte. Quando chiami un composable più volte dallo stesso sito di chiamata, Compose non dispone di informazioni per identificare in modo univoco ogni chiamata al composable, pertanto l'ordine di esecuzione viene utilizzato oltre al sito di chiamata per mantenere distinta le istanze. Questo comportamento a volte è tutto ciò che serve, 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 di chiamata per mantenere distinta l'istanza nella composizione. Se un nuovo movie viene aggiunto in fondo all'elenco, Compose può riutilizzare le istanze già presenti in Composizione poiché la loro posizione nell'elenco non è cambiata e, di conseguenza, l'input movie è lo stesso per queste istanze.

Diagramma che mostra come il codice precedente viene ricomposto se un nuovo elemento viene aggiunto alla fine dell&#39;elenco. Gli altri elementi nell&#39;elenco non hanno subito modifiche di posizione e non sono stati ricomposti.

Figura 4. Rappresentazione di MoviesScreen nella composizione quando un nuovo elemento viene aggiunto in fondo all'elenco. MovieOverview elementi componibili nella Composizione possono essere riutilizzati. Lo stesso colore in MovieOverview indica che il composable non è stato ricomposto.

Tuttavia, se l'elenco movies viene modificato aggiungendolo all'inizio o al centro dell'elenco, rimuovendo o riordinando gli elementi, verrà causata 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 ricompozione avviene mentre l'effetto è in corso, verrà annullata e riavviata.

@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)

        /* ... */
    }
}

Diagramma che mostra come il codice precedente viene ricomposto se un nuovo elemento viene aggiunto all&#39;inizio dell&#39;elenco. Ogni altro elemento nell&#39;elenco cambia posizione e deve essere ricomposto.

Figura 5. Rappresentazione di MoviesScreen nella composizione quando un nuovo elemento viene aggiunto all'elenco. I composabili MovieOverview non possono essere riutilizzati e tutti gli effetti collaterali verranno riavviati. Un colore diverso in MovieOverview indica che il componibile è stato ricomposto.

Idealmente, vogliamo considerare l'identità dell'istanza MovieOverview come collegata all'identità del movie che le viene passato. Se riordinassimo l'elenco di film, idealmente riordineremmo allo stesso modo le istanze nell' albero di composizione anziché ricomporre ogni componibile MovieOverview con un'altra istanza di film. Compose ti consente di indicare al runtime quali valori vuoi utilizzare per identificare una determinata parte dell'albero: il composable key.

Se inserisci un blocco di codice con una chiamata alla chiave composable con uno o più valori passati, questi valori verranno combinati per essere utilizzati per identificare l'istanza nella composizione. Il valore di key non deve essere univoco a livello globale, ma deve essere univoco solo tra le chiamate degli elementi componibili sul sito della chiamata. Quindi, in questo esempio, ogni movie deve avere un key unico tra movies; va bene se condivide key con un altro componibile 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 sopra, anche se gli elementi dell'elenco cambiano, Compose riconosce le singole chiamate a MovieOverview e può riutilizzarle.

Diagramma che mostra come il codice precedente viene ricomposto se un nuovo elemento viene aggiunto all&#39;inizio dell&#39;elenco. Poiché le voci dell&#39;elenco sono identificate da tasti, Compose non sa di ricomporle, anche se la loro posizione è cambiata.

Figura 6. Rappresentazione di MoviesScreen nella composizione quando un nuovo elemento viene aggiunto all'elenco. Poiché i composabili MovieOverview hanno chiavi univoche, Compose riconosce le istanze MovieOverview che non sono cambiate e può riutilizzarle. I relativi effetti collaterali continueranno a essere eseguiti.

Alcuni composabili hanno il supporto integrato per il composable key. Ad esempio, LazyColumn accetta la specifica di un key personalizzato nel 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 ricompozione, l'esecuzione di alcune funzioni composable idonee può essere saltata del tutto se i relativi input non sono cambiati rispetto alla composizione precedente.

Una funzione componibile è idonea per essere saltata a meno che:

  • La funzione ha un tipo di ritorno diverso da Unit
  • La funzione è annotata con @NonRestartableComposable o @NonSkippableComposable
  • Un parametro obbligatorio è di tipo non stabile

Esiste una modalità di compilazione sperimentale, Strong Skipping, che riduce 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 due istanze.
  • Se una proprietà pubblica del tipo cambia, Composition 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 Compose tratterà come stabili, anche se non sono contrassegnati esplicitamente 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 possono seguire il contratto di stabile perché sono immutabili. Dal momento che i tipi immutabili non cambiano mai, non devono mai comunicare alla modifica alla composizione, quindi è molto più facile seguire questo contratto.

Un tipo notevole che è stabile, ma è mutabile, è il tipo MutableState di Compose. Se un valore è memorizzato in un MutableState, l'oggetto stato complessivo è considerato stabile in quanto Compose riceverà una notifica di eventuali modifiche alla proprietà .value di State.

Quando tutti i tipi passati come parametri a un composable sono stabili, i valori dei parametri vengono confrontati per verificare l'uguaglianza in base alla posizione del composable nell'albero dell'interfaccia utente. La ricomposizione viene saltata 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 lo stesso vale per i tipi con proprietà pubbliche mutabili la cui implementazione potrebbe essere immutabile.

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 solitamente considerare questo tipo non stabile. Aggiungendo l'annotazione @Stable, indichi a Compose che questo tipo è stabile, consentendogli di favorire le ricostruzioni intelligenti. Ciò significa anche che Compose tratterà tutte le sue implementazioni come stabili se viene utilizzata l'interfaccia come tipo di parametro.