State e Jetpack Compose

Lo stato in un'app è qualsiasi valore che può cambiare nel tempo. Si tratta di una definizione molto ampia che comprende tutto, da un database Room a una variabile in una classe.

Tutte le app Android mostrano lo stato all'utente. Ecco alcuni esempi di stato nelle app Android:

  • Una barra di notifica che viene visualizzata quando non è possibile stabilire una connessione di rete.
  • Un post del blog e i commenti associati.
  • Animazioni di ondulazione sui pulsanti che vengono riprodotte quando un utente li fa clic.
  • Adesivi che un utente può disegnare sopra un'immagine.

Jetpack Compose ti aiuta a specificare dove e come memorizzi e utilizzi lo stato in un'app per Android. Questa guida si concentra sul collegamento tra stato e composabili e sulle API offerte da Jetpack Compose per lavorare con lo stato più facilemente.

Stato e composizione

Il comando Compose è dichiarativo e come tale l'unico modo per aggiornarlo è chiamare lo stesso componibile con nuovi argomenti. Questi argomenti sono rappresentazioni dello stato dell'UI. Ogni volta che viene aggiornato uno stato, viene eseguita una ricostituzione. Di conseguenza, elementi come TextField non si aggiornano automaticamente come nelle visualizzazioni imperative basate su XML. A un composable deve essere comunicato esplicitamente il nuovo stato per consentirgli di aggiornarsi di conseguenza.

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

Se esegui questo codice e provi a inserire del testo, non succederà nulla. Questo accade perché TextField non si aggiorna autonomamente, ma quando cambia il parametro value. Ciò è dovuto al funzionamento della composizione e della ricostituzione in Compose.

Per scoprire di più sulla composizione e la ricomposizione iniziali, consulta Pensare in Compose.

Stato nei composabili

Le funzioni componibili possono utilizzare l'API remember per memorizzare un oggetto in memoria. Un valore calcolato da remember viene memorizzato nella composizione durante la composizione iniziale e il valore memorizzato viene restituito durante la ricostituzione. remember può essere utilizzato per archiviare oggetti mutabili e immutabili.

mutableStateOf crea un osservato MutableState<T>, che è un tipo di osservato integrato con il runtime di compose.

interface MutableState<T> : State<T> {
    override var value: T
}

Eventuali modifiche a value pianificano la ricompozione di eventuali funzioni composable che leggono value.

Esistono tre modi per dichiarare un oggetto MutableState in un composable:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

Queste dichiarazioni sono equivalenti e sono fornite come somma di sintassi per i diversi usi dello stato. Devi scegliere quello che genera il codice più facile da leggere nel composable che stai scrivendo.

La sintassi del delegato by richiede le seguenti importazioni:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Puoi utilizzare il valore memorizzato come parametro per altri composabili o persino come logica nelle istruzioni per modificare i composabili visualizzati. Ad esempio, se non vuoi mostrare il saluto se il nome è vuoto, utilizza lo stato in un statement if:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

Mentre remember ti aiuta a mantenere lo stato nelle ricomposizioni, lo stato non viene mantenuto nelle modifiche alla configurazione. Per farlo, devi utilizzare rememberSaveable. rememberSaveable salva automaticamente qualsiasi valore salvabile in un Bundle. Per altri valori, puoi trasferire un oggetto salvaschermo personalizzato.

Altri tipi di stato supportati

Compose non richiede l'utilizzo di MutableState<T> per mantenere lo stato, ma supporta altri tipi di oggetti osservabili. Prima di leggere un altro tipo di elemento osservabile in Compose, devi convertirlo in State<T> in modo che i componenti composibili possano ricomporsi automaticamente quando cambia lo stato.

Compose viene fornito con funzioni per creare State<T> da tipi osservabili comuni utilizzati nelle app per Android. Prima di utilizzare queste integrazioni, aggiungi gli elementi appropriati come descritto di seguito:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() raccoglie i valori da un Flow tenendo conto del ciclo di vita, consentendo alla tua app di risparmiare risorse dell'app. Rappresenta l'ultimo valore emesso da Compose State. Utilizza questa API come metodo consigliato per raccogliere i flussi nelle app per Android.

    Nel file build.gradle è richiesta la seguente dipendenza (deve essere 2.6.0-beta01 o successiva):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.5"
}
  • Flow: collectAsState()

    collectAsState è simile a collectAsStateWithLifecycle, perché raccoglie anche i valori da un Flow e li trasforma in Compose State.

    Utilizza collectAsState per il codice indipendente dalla piattaforma anziché collectAsStateWithLifecycle, che è solo per Android.

    Non sono necessarie dipendenze aggiuntive per collectAsState, in quanto è disponibile in compose-runtime.

  • LiveData: observeAsState()

    observeAsState() inizia a osservare questo LiveData e ne rappresenta i valori tramite State.

    Nel file build.gradle è obbligatoria la seguente dipendenza:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.3")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.7.3"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.7.3")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.7.3"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.7.3")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.7.3"
}

Stateful e stateless

Un composable che utilizza remember per archiviare un oggetto crea uno stato interno, rendendo il composable con stato. HelloContent è un esempio di composable con stato perché gestisce e modifica internamente il proprio stato name. Questo può essere utile in situazioni in cui chi chiama non ha bisogno di controllare lo stato e può utilizzarlo senza dover gestire lo stato stesso. Tuttavia, i componenti componibili con stato interno tendono a essere meno riutilizzabili e più difficili da testare.

Un composable senza stato è un composable che non memorizza alcun stato. Un modo semplice per ottenere uno stato stateless è utilizzare l'elevatore di stato.

Quando sviluppi composabili riutilizzabili, spesso vuoi esporre sia una versione con stato sia una senza stato dello stesso composable. La versione con stato è comoda per gli utenti chiamanti che non si preoccupano dello stato, mentre la versione senza stato è necessaria per gli utenti chiamanti che devono controllare o sollevare lo stato.

Innalzamento dello stato

L'innalzamento dello stato in Compose è un modello di spostamento dello stato al chiamante di un componibile per rendere uno stateless componibile. Il pattern generale per il sollevamento dello stato in Jetpack Compose consiste nel sostituire la variabile di stato con due parametri:

  • value: T: il valore corrente da visualizzare
  • onValueChange: (T) -> Unit: un evento che richiede la modifica del valore, dove T è il nuovo valore proposto

Tuttavia, non sei limitato a onValueChange. Se per il composable sono appropriati eventi più specifici, devi definirli utilizzando le lambda.

Lo stato sollevato in questo modo ha alcune proprietà importanti:

  • Unica fonte attendibile: spostando lo stato anziché duplicarlo, garantiamo che esista un'unica fonte attendibile. In questo modo puoi evitare bug.
  • Incapsulati:solo i composabili con stato possono modificarlo. È completamente interno.
  • Condiviso:lo stato in primo piano può essere condiviso con più composabili. Se vuoi leggere name in un componibile diverso, sollevamento consente di farlo.
  • Intercettabili:gli utenti che chiamano i composabili stateless possono decidere di ignorare o modificare gli eventi prima di modificare lo stato.
  • Scollegato:lo stato dei composabili senza stato può essere memorizzato ovunque. Ad esempio, ora è possibile spostare name in un ViewModel.

Nell'esempio, estrai name e onValueChange da HelloContent e li sposti verso l'alto dell'albero in un composable HelloScreen che chiama HelloContent.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

Se estrai lo stato da HelloContent, è più facile ragionare sul composable, riutilizzarlo in situazioni diverse e testarlo. HelloContent è indipendente dal modo in cui viene archiviato il relativo stato. Il disaccoppiamento significa che se modifichi o sostituisci HelloScreen, non devi cambiare la modalità di implementazione di HelloContent.

Il pattern in cui lo stato diminuisce e gli eventi aumentano è chiamato flusso di dati unidirezionale. In questo caso, lo stato passa da HelloScreen a HelloContent e gli eventi aumentano da HelloContent a HelloScreen. Seguendo il flusso di dati unidirezionale, puoi disaccoppiare i composabili che mostrano lo stato nell'interfaccia utente dalle parti dell'app che memorizzano e modificano lo stato.

Per scoprire di più, consulta la pagina Dove eseguire l'hoisting dello stato.

Ripristino dello stato in Componi

L'API rememberSaveable si comporta in modo simile a remember perché conserva lo stato durante le ricostruzioni e anche durante la ricreazione di attività o processi utilizzando il meccanismo dello stato dell'istanza salvato. Ad esempio, accade quando lo schermo viene ruotato.

Modalità di archiviazione dello stato

Tutti i tipi di dati aggiunti a Bundle vengono salvati automaticamente. Se vuoi salvare qualcosa che non può essere aggiunto a Bundle, hai a disposizione diverse opzioni.

Parcellare

La soluzione più semplice è aggiungere l'annotazione @Parcelize all'oggetto. L'oggetto diventa "parcelabile" e può essere aggregato. Ad esempio, questo codice crea un tipo di dati City parcellabile e lo salva nello stato.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

Se per qualche motivo @Parcelize non è adatto, puoi utilizzare mapSaver per definire la tua regola per convertire un oggetto in un insieme di valori che il sistema può salvare in Bundle.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

Per evitare di dover definire le chiavi della mappa, puoi anche utilizzare listSaver e usare i relativi indici come chiavi:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Detentori dello stato in Compose

Il semplice sollevamento dello stato può essere gestito nelle funzioni componibili stesse. Tuttavia, se la quantità di stato da monitorare aumenta o se si presenta la logica da eseguire nelle funzioni componibili, è buona prassi delegare le responsabilità relative alla logica e allo stato ad altri oggetti: i contenitori di stato.

Per scoprire di più, consulta la documentazione sull'elevatore dello stato in Compose o, più in generale, la pagina Contenitori dello stato e stato dell'interfaccia utente nella guida all'architettura.

Riattivare i calcoli di Ricorda quando le chiavi cambiano

L'API remember viene spesso utilizzata insieme a MutableState:

var name by remember { mutableStateOf("") }

In questo caso, l'utilizzo della funzione remember fa sì che il valore MutableState sopravviva alle ricomposizioni.

In generale, remember accetta un parametro lambda calculation. Quando remember viene eseguito per la prima volta, richiama la funzione lambda calculation e ne memorizza il risultato. Durante la recomposizione, remember restituisce l'ultimo valore memorizzato.

Oltre a memorizzare nella cache lo stato, puoi utilizzare remember anche per archiviare qualsiasi oggetto o risultato di un'operazione nella composizione che è costoso da inizializzare o calcolare. Non è consigliabile ripetere questo calcolo per ogni ricomposizione. Un esempio è la creazione di questo oggetto ShaderBrush, un'operazione costosa:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

remember memorizza il valore finché non esce dalla composizione. Tuttavia, esiste un modo per invalidare il valore memorizzato nella cache. L'API remember accetta anche un parametro key o keys. Se una di queste chiavi cambia, la volta successiva che la funzione si ricompone, remember invalida la cache ed esegue di nuovo il calcolo del blocco lambda. Questo meccanismo ti consente di controllare il ciclo di vita di un oggetto nella composizione. Il calcolo rimane valido fino a quando gli input non cambiano, invece che fino a quando il valore memorizzato non lascia la composizione.

I seguenti esempi mostrano come funziona questo meccanismo.

In questo snippet viene creato un elemento ShaderBrush, che viene utilizzato come sfondo di un componibile Box. remember archivia l'istanza ShaderBrush perché è costosa da ricreare, come spiegato in precedenza. remember accettaavatarRes come parametro key1, ovvero l'immagine di sfondo selezionata. Se avatarRes cambia, il pennello si ricomporrà con la nuova immagine e verrà riapplicato a Box. Questo può accadere quando l'utente seleziona un'altra immagine da usare come sfondo da un selettore.

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(
        modifier = modifier.background(brush)
    ) {
        /* ... */
    }
}

Nello snippet successivo, lo stato viene trasferito a una classe di stato normale MyAppState. Espone una funzione rememberMyAppState per inizializzare un'istanza della classe utilizzando remember. L'esposizione di queste funzioni per creare un'istanza che sopravviva alle ricostruzioni è uno schema comune in Compose. La funzione rememberMyAppState riceve windowSizeClass, che funge da parametro key per remember. Se questo parametro cambia, l'app deve rielaborare la classe del detentore dello stato normale con il valore più recente. Ciò può verificarsi se, ad esempio, l'utente ruota il dispositivo.

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

Compose utilizza l'implementazione di equals della classe per decidere se una chiave è stata modificata e per invalidare il valore memorizzato.

Memorizzare lo stato con chiavi oltre la ricostituzione

L'API rememberSaveable è un wrapper di remember che può memorizzare i dati in un Bundle. Questa API consente allo stato di sopravvivere non solo alla ricomposizione, ma anche alla ricreazione dell'attività e alla morte di processi avviati dal sistema. rememberSaveable riceve i parametri input per lo stesso scopo per cui remember riceve keys. La cache viene invalidata quando uno degli input cambia. La volta successiva che la funzione si ricomporrà, rememberSaveable eseguirà nuovamente il blocco lambda di calcolo.

Nell'esempio seguente, rememberSaveable memorizza userTypedQuery finché typedQuery non cambia:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

Scopri di più

Per scoprire di più sullo stato e su Jetpack Compose, consulta le seguenti risorse aggiuntive.

Campioni

Codelab

Video

Blog