State e Jetpack Compose

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

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

  • Uno Snackbar che viene visualizzato quando non è possibile stabilire una connessione di rete.
  • Un post del blog e commenti associati.
  • Crea animazioni onde sui pulsanti che vengono riprodotti quando un utente li fa clic.
  • Adesivi che un utente può disegnare sopra un'immagine.

Jetpack Compose ti consente di indicare in modo esplicito dove e come archiviare e utilizzare lo stato in un'app per Android. Questa guida si concentra sulla connessione tra stato e componibili e sulle API che Jetpack Compose offre per lavorare con lo stato in modo più semplice.

Stato e composizione

Compose è dichiarativo e, di conseguenza, l'unico modo per aggiornarlo è chiamare lo stesso componibile con nuovi argomenti. Questi argomenti sono rappresentazioni dello stato dell'UI. Ogni volta che uno stato viene aggiornato, avviene una ricomposizione. Di conseguenza, elementi come TextField non si aggiornano automaticamente come avviene nelle viste imperative basate su XML. Un componibile deve ricevere esplicitamente il nuovo stato per aggiornarlo 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 questa azione e provi a inserire testo, noterai che non succede nulla. Questo perché TextField non si aggiorna automaticamente, ma quando il parametro value cambia. Ciò è dovuto al funzionamento della composizione e della ricomposizione in Compose.

Per scoprire di più sulla composizione e la ricomposizione iniziale, vedi Pensare in Compose.

Stato nei componibili

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 archiviato viene restituito durante la ricomposizione. remember può essere utilizzato per archiviare oggetti sia modificabili che immutabili.

mutableStateOf crea un elemento MutableState<T> osservabile, ovvero un tipo osservabile integrato con il runtime di scrittura.

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

Qualsiasi modifica a value pianifica la ricomposizione di qualsiasi funzione componibile che legge value.

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

  • 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 della sintassi per diversi usi dello stato. Devi scegliere quella che produce il codice più facile da leggere nel componibile che stai scrivendo.

La sintassi delegata by richiede le seguenti importazioni:

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

Puoi utilizzare il valore memorizzato come parametro per altri componibili o anche come logica nelle istruzioni per modificare gli elementi 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") }
        )
    }
}

Mentre remember consente di mantenere lo stato nelle ricomposizioni, lo stato non viene conservato per tutte le modifiche alla configurazione. Per farlo, devi usare rememberSaveable. rememberSaveable salva automaticamente qualsiasi valore salvato in una Bundle. Per altri valori, puoi passare un oggetto salvaschermo personalizzato.

Altri tipi di stato supportati

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

Compose include funzioni per creare State<T> dai tipi osservabili 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 elemento Flow in modo consapevole per il ciclo di vita, consentendo alla tua app di conservare le risorse dell'app. Rappresenta l'ultimo valore emesso da Scrivi State. Utilizza questa API come metodo consigliato per raccogliere flussi nelle app per Android.

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

Kotlin

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

trendy

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

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

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

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

  • LiveData: observeAsState()

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

    La seguente dipendenza è obbligatoria nel file build.gradle:

Kotlin

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

trendy

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

Kotlin

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

trendy

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

Kotlin

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

trendy

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

Stateful e stateless

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

Un componibile stateless è un componibile privo di stato. Un modo semplice per ottenere l'stateless è utilizzare il riporto dello stato.

Durante lo sviluppo di elementi componibili riutilizzabili, spesso è consigliabile esporre sia una versione stateful che una stateless dello stesso componibile. La versione stateful è comoda per i chiamanti che non sono interessati allo stato, mentre la versione stateless è necessaria per i chiamanti che devono controllare o istruire lo stato.

Sollevamento statale

L'installazione di stato in Compose è uno schema di spostamento dello stato al chiamante di un componibile per rendere un oggetto componibile stateless. Il pattern generale per l'installazione di stato in Jetpack Compose è la sostituzione della variabile di stato con due parametri:

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

Tuttavia, non hai limitazioni a onValueChange. Se eventi più specifici sono appropriati per il componibile, devi definirli utilizzando le lambda.

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

  • Un'unica fonte di verità: spostando lo stato anziché duplicarlo, garantiamo che esista un'unica fonte attendibile. In questo modo eviterai i bug.
  • Incapsulato: solo gli elementi componibili stateful possono modificarne lo stato. È completamente interno.
  • Condividibile:lo stato sollevato può essere condiviso con più elementi componibili. Se vuoi leggere name in un altro componibile, sollevarlo ti consente di farlo.
  • Intercettabile:i chiamanti dei componibili stateless possono decidere di ignorare o modificare gli eventi prima di cambiare lo stato.
  • Decuplicato: lo stato degli elementi componibili stateless può essere archiviato ovunque. Ad esempio, ora è possibile spostare name in un ViewModel.

Nel caso di esempio, estrai name e onValueChange da HelloContent e li sposti nella struttura ad albero in un 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 componibile, riutilizzarlo in diverse situazioni ed eseguire test. HelloContent è disaccoppiato dalla modalità di archiviazione del suo stato. Con il disaccoppiamento, se modifichi o sostituisci HelloScreen, non devi cambiare la modalità di implementazione di HelloContent.

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

Per ulteriori informazioni, consulta la pagina Dove controllare lo stato.

Ripristino dello stato in Compose

L'API rememberSaveable si comporta in modo simile a remember perché conserva lo stato nelle ricomposizioni e anche nell'attività o nella creazione di processi utilizzando il meccanismo dello stato dell'istanza salvata. ad esempio quando lo schermo viene ruotato.

Modalità di archiviazione dello stato

Tutti i tipi di dati aggiunti a Bundle vengono salvati automaticamente. Se vuoi salvare un elemento che non può essere aggiunto in Bundle, esistono diverse opzioni.

Particella

La soluzione più semplice consiste nell'aggiungere l'annotazione @Parcelize all'oggetto. L'oggetto diventa componibile e può essere associato. Ad esempio, questo codice crea un tipo di dati City comparabile 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"))
    }
}

Salva mappa

Se per qualche motivo @Parcelize non è adatto, puoi utilizzare mapSaver per definire una tua regola per convertire un oggetto in un insieme di valori che il sistema può salvare nel 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"))
    }
}

Risparmio elenco

Per evitare di dover definire le chiavi per la mappa, puoi anche utilizzare listSaver e i suoi 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 emerge la quantità di stato da tenere traccia degli aumenti o la logica da eseguire nelle funzioni componibili, è buona norma delegare le responsabilità logiche e di stato ad altre classi: proprietari di stato.

Per saperne di più, consulta la documentazione relativa all'allestimento dello stato in Compose o, più in generale, la pagina Proprietari di stato e stato dell'interfaccia utente nella guida all'architettura.

Riattivare il salvataggio dei calcoli quando le chiavi cambiano

L'API remember viene spesso utilizzata insieme a MutableState:

var name by remember { mutableStateOf("") }

In questo caso, l'uso della funzione remember consente al valore di MutableState di sopravvivere alle ricomposizioni.

In generale, remember richiede un parametro lambda calculation. Quando viene eseguita per la prima volta, remember richiama la funzione lambda calculation e archivia il risultato. Durante la ricomposizione, remember restituisce il valore archiviato per l'ultima volta.

Oltre allo stato della memorizzazione nella cache, puoi anche usare remember per archiviare qualsiasi oggetto o risultato di un'operazione nella composizione costosa da inizializzare o calcolare. Potresti non voler ripetere questo calcolo a 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 fino a quando non lascia la composizione. Tuttavia, esiste un modo per invalidare il valore memorizzato nella cache. L'API remember richiede anche un parametro key o keys. Se una di queste chiavi cambia, la prossima volta che la funzione viene ricomposta, remember non convalida la cache ed esegue di nuovo il blocco lambda di calcolo. Questo meccanismo ti permette di controllare la durata di un oggetto nella composizione. Il calcolo rimane valido finché gli input non cambiano, anziché fino a quando il valore memorizzato non lascia la composizione.

I seguenti esempi mostrano il funzionamento di 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 richiede avatarRes come parametro key1, che corrisponde all'immagine di sfondo selezionata. Se il valore avatarRes cambia, il pennello si ricompone con la nuova immagine e viene riapplicato a Box. Questo può accadere 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)
    ) {
        /* ... */
    }
}

Nello snippet successivo, lo stato viene iscritta a una classe titolare stato normale MyAppState. Espone una funzione rememberMyAppState per inizializzare un'istanza della classe utilizzando remember. L'esposizione di queste funzioni per creare un'istanza in grado di sopravvivere alle ricomposizioni è un modello 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 titolare dello stato normale con il valore più recente. Questo può accadere 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 uguale a della classe per decidere se una chiave è stata modificata e invalidarne il valore archiviato.

Archivia lo stato con le chiavi oltre la ricomposizione

L'API rememberSaveable è un wrapper attorno a remember in grado di archiviare i dati in una Bundle. Questa API consente allo stato di sopravvivere non solo alla ricomposizione, ma anche alla ricomposizione dell'attività e alla morte dei processi avviati dal sistema. rememberSaveable riceve i parametri input per lo stesso scopo per cui remember riceve keys. La cache viene invalidata quando cambia uno degli input. La prossima volta che la funzione si ricompone, rememberSaveable riesegui il blocco lambda di calcolo.

Nel seguente esempio, rememberSaveable archivia userTypedQuery fino a quando 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ù sullo stato e su Jetpack Compose, consulta le seguenti risorse aggiuntive.

Campioni

Codelab

Video

Blog