Ciclo di vita dei componibili

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.

Diagramma che mostra il ciclo di vita di un componibile

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

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

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.

Diagramma che mostra come viene ricomposto il codice precedente se il flag showError viene impostato su true. L&#39;elemento componibile LoginError viene aggiunto, ma gli altri elementi componibili 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 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.

Diagramma che mostra come viene ricomposto il codice precedente se viene aggiunto un nuovo elemento in fondo all&#39;elenco. Gli altri elementi nell&#39;elenco non hanno cambiato posizione e non sono stati ricomposti.

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)

        /* ... */
    }
}

Diagramma che mostra come viene ricomposto il codice precedente se viene aggiunto un nuovo elemento 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 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.

Diagramma che mostra come viene ricomposto il codice precedente se viene aggiunto un nuovo elemento all&#39;inizio dell&#39;elenco. Poiché gli elementi dell&#39;elenco sono identificati da chiavi, Compose non sa di 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é 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.