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 unFlow
tenendo conto del ciclo di vita, consentendo alla tua app di risparmiare risorse dell'app. Rappresenta l'ultimo valore emesso da ComposeState
. 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"
}
-
collectAsState
è simile acollectAsStateWithLifecycle
, perché raccoglie anche i valori da unFlow
e li trasforma in ComposeState
.Utilizza
collectAsState
per il codice indipendente dalla piattaforma anzichécollectAsStateWithLifecycle
, che è solo per Android.Non sono necessarie dipendenze aggiuntive per
collectAsState
, in quanto è disponibile incompose-runtime
. -
observeAsState()
inizia a osservare questoLiveData
e ne rappresenta i valori tramiteState
.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"
}
-
subscribeAsState()
sono funzioni di estensione che trasformano gli stream reattivi di RxJava2 (ad es.Single
,Observable
,Completable
) in ComposeState
.Nel file
build.gradle
è obbligatoria la seguente dipendenza:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.3"
}
-
subscribeAsState()
sono funzioni di estensione che trasformano gli stream reattivi di RxJava3 (ad es.Single
,Observable
,Completable
) in ComposeState
.Nel file
build.gradle
è obbligatoria la seguente dipendenza:
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 visualizzareonValueChange: (T) -> Unit
: un evento che richiede la modifica del valore, doveT
è 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 unViewModel
.
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
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Architettura dell'interfaccia utente di Scrittura
- Salvare lo stato dell'interfaccia utente in Scrivi
- Effetti collaterali in Componi