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
Compose è dichiarativo e, pertanto, l'unico modo per aggiornarlo è chiamare lo stesso composable con nuovi argomenti. Questi argomenti sono rappresentazioni dello stato dell'interfaccia utente. 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 comando e provi a inserire del testo, vedrai che non succede niente. 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 iniziale e sulla ricompozione, consulta Pensare in Compose.
Stato nei componibili
Le funzioni componibili possono utilizzare l'API
remember
per memorizzare un oggetto in memoria. Un valore calcolato da remember
viene archiviato nella composizione durante la composizione iniziale e il valore archiviato viene restituito durante la ricomposizione.
È possibile utilizzare remember
per archiviare oggetti sia mutabili che 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 vengono fornite come sintassi alternativa per diversi utilizzi dello stato. Scegli quello che produce il codice più facile da leggere nel componibile 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") } ) } }
Sebbene remember
ti aiuti a mantenere lo stato durante le ricostruzioni, lo stato non viene conservato durante le modifiche alla configurazione. Per farlo, devi utilizzare
rememberSaveable
. rememberSaveable
salva automaticamente qualsiasi valore che può essere memorizzato 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 conservare lo stato; supporta altri tipi 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 sulle app 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.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
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
, perché è 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.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}
-
subscribeAsState()
sono funzioni di estensione che trasformano i flussi 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.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}
-
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.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}
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 composabili 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 elementi componibili riutilizzabili, spesso desideri esporre sia una versione stateful che una versione 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 aumentare lo stato.
Innalzamento dello stato
L'elevazione dello stato in Compose è un pattern che sposta lo stato all'autore di un composable per creare un composable senza stato. Il pattern generale per l'elevazione dello stato in Jetpack Compose è 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.
- Disaccoppiato: lo stato dei componenti componibili stateless può essere archiviato 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 Compose
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, questo accade quando
lo schermo viene ruotato.
Modalità di memorizzazione 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 consiste nell'aggiungere
l'annotazione
@Parcelize
all'oggetto. L'oggetto diventa frazionabile e può essere raggruppato. Ad esempio, questo codice crea un tipo di dati City
partizionabili 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")) } }
Salvatore Map
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 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.
Riattiva i calcoli di memorizzazione quando le chiavi cambiano
L'API remember
viene utilizzata spesso insieme a MutableState
:
var name by remember { mutableStateOf("") }
In questo caso, l'utilizzo della funzione remember
consente al valore MutableState
di sopravvivere alle ricostruzioni.
In generale, remember
richiede 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. Potresti non voler ripetere questo calcolo in ogni ricompozione.
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, alla successiva ricomposizione della funzione, remember
non convalida la cache ed esegue nuovamente 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, anziché fino a quando il valore memorizzato non esce dalla 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
memorizza l'istanza ShaderBrush
perché è costoso ricrearla, 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. Questo può accadere, ad esempio, se 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 ricompozione, ma anche alla ricreazione delle 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 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