State e Jetpack Compose

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

Tutte le app per Android mostrano lo stato all'utente. Alcuni esempi di stato nelle app per Android:

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

Jetpack Compose ti aiuta a definire in modo esplicito dove e come archiviare e utilizzare lo stato in un'app per Android. Questa guida si concentra sulla connessione tra lo stato e i composable e sulle API che Jetpack Compose offre per lavorare più facilmente con lo stato.

Stato e composizione

Compose è dichiarativo e, pertanto, l'unico modo per aggiornarlo è chiamare lo stesso elemento componibile con nuovi argomenti. Questi argomenti sono rappresentazioni dello stato dell'interfaccia utente. Ogni volta che uno stato viene aggiornato, si verifica una ricomposizione. Di conseguenza, elementi come TextField non vengono aggiornati automaticamente come nelle visualizzazioni basate su XML imperativo. Per aggiornarsi di conseguenza, a un elemento componibile deve essere comunicato esplicitamente il nuovo stato.

@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 comando e provi a inserire del testo, vedrai che non succede nulla. Questo perché TextField non si aggiorna automaticamente, ma quando cambia il parametro value. Ciò è dovuto al modo in cui funzionano la composizione e la ricomposizione in Compose.

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

Stato nei composable

Le funzioni componibili possono utilizzare l'API remember per archiviare 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 ricomposizione. remember può essere utilizzato per archiviare oggetti modificabili e immutabili.

mutableStateOf crea un oggetto osservabile MutableState<T>, che è un tipo osservabile integrato con il runtime di Compose.

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

Qualsiasi modifica alle pianificazioni di value comporta la ricomposizione di qualsiasi funzione componibile che legge 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 vengono fornite come zucchero sintattico per diversi utilizzi dello stato. Devi scegliere quello che produce il codice più facile da leggere nel composable che stai scrivendo.

La sintassi del delegato by richiede i seguenti import:

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

Puoi utilizzare il valore memorizzato come parametro per altri componenti componibili o anche come logica nelle istruzioni per modificare i componenti componibili visualizzati. Ad esempio, se non vuoi visualizzare il saluto se il nome è vuoto, utilizza lo stato in un'istruzione 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") }
        )
    }
}

Sebbene remember ti aiuti a mantenere lo stato durante le ricomposizioni, lo stato non viene mantenuto durante le modifiche alla configurazione. Per farlo, devi utilizzare rememberSaveable. rememberSaveable salva automaticamente qualsiasi valore che può essere salvato in un Bundle. Per gli altri valori, puoi passare un oggetto di salvataggio personalizzato.

Altri tipi di stato supportati

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

Compose è dotato di funzioni per creare State<T> dai tipi di observable comuni utilizzati nelle app per Android. Prima di utilizzare queste integrazioni, aggiungi gli artefatti appropriati, come descritto di seguito:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() raccoglie i valori da un Flow in modo consapevole del ciclo di vita, consentendo alla tua app di conservare le 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.

    La seguente dipendenza è richiesta nel file build.gradle (deve essere 2.6.0-beta01 o versioni successive):

Kotlin

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

Groovy

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

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

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

    Non sono richieste dipendenze aggiuntive per collectAsState, perché è disponibile in compose-runtime.

  • LiveData: observeAsState()

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

    La seguente dipendenza è obbligatoria nel file build.gradle:

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Stateful e stateless

Un composable che utilizza remember per archiviare un oggetto crea uno stato interno, rendendo il composable stateful. HelloContent è un esempio di composizione con stato perché contiene e modifica internamente il suo stato name. Può essere utile in situazioni in cui un chiamante non ha bisogno di controllare lo stato e può utilizzarlo senza doverlo gestire personalmente. Tuttavia, i composable con stato interno tendono a essere meno riutilizzabili e più difficili da testare.

Un composable senza stato è un composable che non contiene alcuno stato. Un modo semplice per ottenere l'assenza di stato è utilizzare l'innalzamento dello stato.

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

Innalzamento dello stato

Il sollevamento dello stato in Compose è un pattern di spostamento dello stato al chiamante di un composable per rendere un composable stateless. Il pattern generale per l'innalzamento 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 componente sono appropriati eventi più specifici, devi definirli utilizzando le espressioni lambda.

Lo stato sottoposto a hoisting in questo modo presenta alcune proprietà importanti:

  • Unica fonte attendibile: spostando lo stato anziché duplicarlo, ci assicuriamo che esista un'unica fonte attendibile. In questo modo si evitano bug.
  • Incapsulati:solo i composable stateful possono modificare il proprio stato. È completamente interno.
  • Condivisibile:lo stato sollevato può essere condiviso con più composable. Se volessi leggere name in un altro elemento componibile, l'hoisting ti consentirebbe di farlo.
  • Intercettabile:i chiamanti dei composable stateless possono decidere di ignorare o modificare gli eventi prima di cambiare lo stato.
  • Disaccoppiato:lo stato dei composable stateless può essere archiviato ovunque. Ad esempio, ora è possibile spostare name in un ViewModel.

Nell'esempio, estrai name e onValueChange da HelloContent e li sposti verso l'alto nella struttura ad albero in un elemento componibile 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") })
    }
}

Sollevando lo stato da HelloContent, è più facile ragionare sul composabile, riutilizzarlo in diverse situazioni e testarlo. HelloContent è disaccoppiato dal modo in cui viene memorizzato il suo stato. Il disaccoppiamento significa che se modifichi o sostituisci HelloScreen, non devi modificare l'implementazione di HelloContent.

Il pattern in cui lo stato diminuisce e gli eventi aumentano è chiamato flusso di dati unidirezionale. In questo caso, lo stato scende da HelloScreen a HelloContent e gli eventi aumentano da HelloContent a HelloScreen. Seguendo il flusso di dati unidirezionale, puoi separare i composable che mostrano lo stato nell'UI dalle parti dell'app che archiviano e modificano lo stato.

Per scoprire di più, consulta la pagina Dove sollevare lo stato.

Ripristino dello stato in Compose

L'API rememberSaveable si comporta in modo simile a remember perché mantiene lo stato tra le ricomposizioni e anche tra le attività o la ricreazione dei processi utilizzando il meccanismo di stato dell'istanza salvata. Ad esempio, questo 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 diverse opzioni.

Parcelize

La soluzione più semplice è aggiungere l'annotazione @Parcelize all'oggetto. L'oggetto diventa trasferibile e può essere raggruppato. Ad esempio, questo codice crea un tipo di dati City parcelable 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 per la mappa, puoi anche utilizzare listSaver e utilizzare 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"))
    }
}

Contenitori di 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 norma delegare la logica e le responsabilità dello stato ad altre classi: i titolari dello stato.

Per scoprire di più, consulta la documentazione relativa all'innalzamento dello stato in Compose o, più in generale, la pagina State holder e stato dell'UI nella guida all'architettura.

Ricalcolare i valori memorizzati quando cambiano le chiavi

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 memorizza il risultato. Durante la ricomposizione, remember restituisce l'ultimo valore memorizzato.

Oltre allo stato della memorizzazione nella cache, puoi utilizzare remember per archiviare qualsiasi oggetto o risultato di un'operazione nella composizione che è costoso inizializzare o calcolare. Potresti non voler ripetere questo calcolo in ogni ricomposizione. Un esempio è la creazione di questo oggetto ShaderBrush, che è 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 ricompone, remember invalida la cache ed esegue di nuovo il blocco lambda di calcolo. Questo meccanismo ti consente di controllare la durata di un oggetto nella composizione. Il calcolo rimane valido finché gli input cambiano, anziché finché il valore memorizzato non esce dalla composizione.

I seguenti esempi mostrano come funziona questo meccanismo.

In questo snippet viene creato e utilizzato un ShaderBrush come sfondo di un composable Box. remember memorizza l'istanza ShaderBrush perché è costoso ricrearla, come spiegato in precedenza. remember accetta avatarRes come parametro key1, ovvero l'immagine di sfondo selezionata. Se avatarRes cambia, il pennello viene ricomposto con la nuova immagine e riapplicato a Box. Ciò può verificarsi quando l'utente seleziona un'altra immagine da utilizzare 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)
    ) {
        /* ... */
    }
}

Nel seguente snippet, lo stato viene sollevato a una classe di contenitore di stato semplice MyAppState. Espone una funzione rememberMyAppState per inizializzare un'istanza della classe utilizzando remember. L'esposizione di queste funzioni per creare un'istanza che sopravvive alle ricomposizioni è un pattern comune in Compose. La funzione rememberMyAppState riceve windowSizeClass, che funge da parametro key per remember. Se questo parametro cambia, l'app deve ricreare la classe di contenitore dello stato semplice 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 equals della classe per decidere se una chiave è cambiata e invalidare il valore memorizzato.

Archiviare lo stato con chiavi oltre la ricomposizione

L'API rememberSaveable è un wrapper intorno a remember che può archiviare i dati in un Bundle. Questa API consente allo stato di sopravvivere non solo alla ricomposizione, ma anche alla ricreazione dell'attività e all'interruzione del processo avviata dal sistema. rememberSaveable riceve i parametri input per lo stesso scopo per cui remember riceve keys. La cache viene invalidata quando uno qualsiasi degli input cambia. La volta successiva che la funzione viene ricomposta, rememberSaveable viene eseguito 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 saperne di più su State e Jetpack Compose, consulta le seguenti risorse aggiuntive.

Campioni

Codelab

Video

Blog