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 elementoFlow
in modo consapevole per il ciclo di vita, consentendo alla tua app di conservare le risorse dell'app. Rappresenta l'ultimo valore emesso da ScriviState
. 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"
}
-
collectAsState
è simile acollectAsStateWithLifecycle
, perché raccoglie anche i valori da un oggettoFlow
e li trasforma in un oggetto ComposeState
.Usa
collectAsState
per il codice indipendente dalla piattaforma anzichécollectAsStateWithLifecycle
, che è disponibile solo per Android.Non sono necessarie dipendenze aggiuntive per
collectAsState
, perché è disponibile incompose-runtime
. -
observeAsState()
inizia a osservareLiveData
e rappresenta i suoi valori tramiteState
.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"
}
-
subscribeAsState()
sono funzioni di estensione che trasformano i flussi reattivi di RxJava2 (ad es.Single
,Observable
,Completable
) in ComposeState
.La seguente dipendenza è obbligatoria nel file
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.6.1")
}
trendy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.6.1"
}
-
subscribeAsState()
sono funzioni di estensione che trasformano i flussi reattivi di RxJava3 (ad es.Single
,Observable
,Completable
) in ComposeState
.La seguente dipendenza è obbligatoria nel file
build.gradle
:
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 visualizzareonValueChange: (T) -> Unit
: un evento che richiede la modifica del valore, in cuiT
è 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 unViewModel
.
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
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Architettura dell'UI di Compose
- Salvare lo stato dell'interfaccia utente in Compose
- Effetti collaterali in Compose