Ciclo di vita dei componibili

In questa pagina scoprirai il ciclo di vita di un componibile e come Compose decide se un componibile deve essere ricomposto.

Panoramica del ciclo di vita

Come indicato nella documentazione sulla gestione dello stato, una composizione descrive l'interfaccia utente della tua app e viene prodotta eseguendo elementi componibili. Una composizione è una struttura ad albero dei componibili che descrivono la tua UI.

Quando Jetpack Compose esegue i tuoi componibili per la prima volta, durante la composizione iniziale, tiene traccia dei componibili 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 esegue nuovamente i componibili che potrebbero essere cambiati in risposta alle modifiche dello stato, quindi 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 è tramite la ricomposizione.

Diagramma che mostra il ciclo di vita di un componibile
Figura 1. Ciclo di vita di un componibile nella composizione. Entra nella composizione, viene ricomposto 0 o più volte ed esce dalla composizione.

La ricomposizione viene in genere attivata da una modifica a un oggetto State<T>. Compose tiene traccia di questi valori ed esegue tutti i componibili nella composizione che leggono quel State<T> specifico, nonché tutti i componibili che chiamano che non possono essere saltati.

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")
    }
}

Diagramma che mostra la disposizione gerarchica degli elementi nello snippet di codice precedente
Figura 2. Rappresentazione di MyComposable nella composizione. Se un componibile viene chiamato più volte, nella composizione vengono inserite più istanze. Un elemento con un colore diverso indica che si tratta di un'istanza separata.

Anatomia di un composable in Composition

L'istanza di un composable 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 componibile chiama componibili diversi rispetto a quelli chiamati durante la composizione precedente, Compose identifica i componibili che sono stati chiamati o meno e, per i componibili che sono stati chiamati in entrambe le composizioni, Compose evita di ricomporli se i relativi input non sono cambiati.

Preservare l'identità è fondamentale per associare gli effetti collaterali al relativo componibile, in modo che possano essere completati correttamente anziché riavviarsi 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 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 composable LoginError, ma gli altri composable non vengono ricomposti.
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 dall'essere chiamata per prima all'essere chiamata per seconda, l'istanza LoginInput verrà conservata tra le ricomposizioni. Inoltre, poiché LoginInput non ha parametri che sono cambiati durante la ricomposizione, la chiamata a LoginInput verrà saltata da Compose.

Aggiungi informazioni aggiuntive per facilitare le ricomposizioni intelligenti

La chiamata di un composable più volte lo aggiungerà anche alla composizione più volte. Quando chiami un composable più volte dallo stesso sito di chiamata, Compose non ha 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 viene aggiunto un nuovo movie in fondo all'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.

Diagramma che mostra come viene ricomposto il codice precedente se viene aggiunto un nuovo elemento in fondo all'elenco. Gli altri elementi dell'elenco non hanno cambiato posizione e non vengono ricomposti.
Figura 4. Rappresentazione di MoviesScreen nella composizione quando viene aggiunto un nuovo elemento in fondo all'elenco. I componibili 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 all'inizio o al centro 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 un'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)

        /* ... */
    }
}

Diagramma che mostra come viene ricomposto il codice precedente se viene aggiunto un nuovo elemento all'inizio dell'elenco. Tutti gli altri elementi dell'elenco cambiano posizione e devono essere ricomposti.
Figura 5. Rappresentazione di MoviesScreen nella composizione quando viene aggiunto un nuovo elemento all'elenco. I componibili 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 pensare all'identità dell'istanza MovieOverview come collegata all'identità di movie che le viene passata. Se riordiniamo l'elenco dei film, idealmente dovremmo riordinare in modo simile le istanze nell'albero di composizione anziché ricomporre ogni componibile MovieOverview con un'istanza di film diversa. Compose ti consente di indicare al runtime quali valori vuoi utilizzare per identificare una determinata parte dell'albero: il componibile key.

Se racchiudi un blocco di codice con una chiamata al composable key 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 chiamate di elementi componibili nel sito di chiamata. In questo esempio, ogni movie deve avere un key univoco tra i movies; non è un problema se condivide questo key con un altro composable altrove nell'app.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

In questo modo, anche se gli elementi dell'elenco cambiano, Compose riconosce le singole chiamate a MovieOverview e può riutilizzarle.

Diagramma che mostra come viene ricomposto il codice precedente se viene aggiunto un nuovo elemento all'inizio dell'elenco. Poiché gli elementi dell'elenco sono identificati da chiavi, Compose sa di non doverli ricomporre, anche se le loro posizioni sono cambiate.
Figura 6. Rappresentazione di MoviesScreen nella composizione quando viene aggiunto un nuovo elemento all'elenco. Poiché i componibili MovieOverview hanno chiavi univoche, Compose riconosce quali istanze di MovieOverview non sono cambiate e può riutilizzarle; i loro effetti collaterali continueranno a essere eseguiti.

Alcuni componibili hanno il supporto integrato per il componibile 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 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 l'omissione a meno che:

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

Esiste una modalità del compilatore, 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à pubblici sono stabili.

Esistono alcuni tipi comuni importanti che rientrano in questo contratto e che il compilatore Compose considererà stabili, anche se non sono contrassegnati esplicitamente come stabili utilizzando l'annotazione @Stable:

  • Tutti i tipi di valori primitivi: Boolean, Int, Long, Float, Char ecc.
  • Stringhe
  • Tutti i tipi di funzione (lambda)

Tutti questi tipi sono in grado di seguire il contratto di stabilità perché sono immutabili. Poiché i tipi immutabili non cambiano mai, non devono mai notificare la modifica alla composizione, quindi è molto più facile seguire questo contratto.

Un tipo degno di nota che è stabile, ma è modificabile è il tipo MutableState di Compose. Se un valore è contenuto in un MutableState, l'oggetto di stato complessivo 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 composable sono stabili, i valori dei parametri vengono confrontati per verificare l'uguaglianza in base alla posizione del composable nell'albero della UI. 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 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 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 normalmente considerare questo tipo non stabile. Aggiungendo l'annotazione @Stable, indichi a Compose che questo tipo è stabile, consentendo a Compose di preferire le ricomposizioni intelligenti. Ciò significa anche che Compose considererà tutte le sue implementazioni come stabili se l'interfaccia viene utilizzata come tipo di parametro.