Lo stato di 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 per Android mostrano lo stato all'utente. Ecco alcuni esempi di stato nelle app per Android:
- Uno snackbar che viene visualizzato 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 essere esplicito su dove e come archiviare e utilizzare lo stato in un'app per Android. Questa guida si concentra sulla connessione tra stato ed elementi componibili e sulle API che Jetpack Compose offre per lavorare più facilmente con lo stato.
Stato e composizione
Compose è dichiarativo e, di conseguenza, l'unico modo per aggiornarlo è chiamare lo stesso elemento componibile con nuovi argomenti. Questi argomenti sono rappresentazioni dello stato della UI. Ogni volta che uno stato viene aggiornato, si verifica una ricomposizione. Di conseguenza, elementi come TextField non si aggiornano automaticamente come nelle visualizzazioni imperative basate su XML. A un elemento componibile deve essere comunicato esplicitamente il nuovo stato affinché si aggiorni 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, vedrai che non succede nulla. Questo perché TextField non si aggiorna automaticamente, ma solo quando il relativo parametro value cambia. Ciò è dovuto al modo in cui funzionano la composizione e la ricomposizione in Compose.
Per saperne di più sulla composizione iniziale e sulla ricomposizione, consulta Pensare in Compose.
Stato negli elementi componibili
Le funzioni componibili possono utilizzare l'
remember
API per archiviare 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.
remember può essere utilizzato per archiviare oggetti modificabili e non modificabili.
mutableStateOf
crea un oggetto osservabile
MutableState<T>,
ovvero un tipo osservabile integrato con il runtime di Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Qualsiasi modifica a value pianifica la ricomposizione di tutte le funzioni componibili che leggono value.
Esistono tre modi per dichiarare un oggetto MutableState in un elemento 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 sintattico per diversi utilizzi dello stato. Dovresti scegliere quella che produce il codice più facile da leggere nell'elemento componibile 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 elementi componibili o persino 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") } ) } }
Sebbene remember ti aiuti a conservare lo stato tra le ricomposizioni, lo stato non viene conservato tra le modifiche alla configurazione. Per questo, devi utilizzare rememberSaveable. rememberSaveable salva automaticamente qualsiasi valore che può essere salvato in un Bundle. Per 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; supporta altri tipi osservabili. Prima di leggere un altro tipo osservabile in
Compose, devi convertirlo in un oggetto State<T> in modo che gli elementi componibili possano
ricomporsi automaticamente quando lo stato cambia.
Compose include funzioni per creare State<T> da 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 unFlowin modo da tenere conto del ciclo di vita, consentendo alla tua app di conservare le 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 versioni successive):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
}
Alla moda
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.10.0"
}
-
collectAsStateè simile acollectAsStateWithLifecycle, perché raccoglie anche i valori da un oggettoFlowe li trasforma in un oggettoStatedi Compose.Utilizza
collectAsStateper il codice indipendente dalla piattaforma anzichécollectAsStateWithLifecycle, che è solo per Android.Non sono richieste dipendenze aggiuntive per
collectAsState, perché è disponibile incompose-runtime. -
observeAsState()inizia a osservare questoLiveDatae ne rappresenta i valori tramiteState.Nel file
build.gradleè richiesta la seguente dipendenza:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.10.5")
}
Alla moda
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.10.5"
}
-
subscribeAsState()sono funzioni di estensione che trasformano gli stream reattivi di RxJava2 (ad es.Single,Observable,Completable) in oggettiStatedi Compose.Nel file
build.gradleè richiesta la seguente dipendenza:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.10.5")
}
Alla moda
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.10.5"
}
-
subscribeAsState()sono funzioni di estensione che trasformano gli stream reattivi di RxJava3 (ad es.Single,Observable,Completable) in oggettiStatedi Compose.Nel file
build.gradleè richiesta la seguente dipendenza:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.10.5")
}
Alla moda
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.10.5"
}
Stateful e stateless
Un elemento componibile che utilizza remember per archiviare un oggetto crea uno stato interno, rendendo l'elemento componibile stateful. HelloContent è un esempio di elemento componibile stateful perché contiene e modifica internamente il relativo 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 autonomamente. Tuttavia, gli elementi componibili con stato interno tendono a essere meno riutilizzabili e più difficili da testare.
Un elemento componibile stateless è un elemento componibile che non contiene alcuno stato. Un modo semplice per ottenere uno stato stateless è utilizzare l'innalzamento dello stato hoisting.
Quando sviluppi elementi componibili riutilizzabili, spesso vuoi esporre sia una versione stateful sia una stateless dello stesso elemento componibile. La versione stateful è comoda per i chiamanti che non si preoccupano dello stato, mentre la versione stateless è necessaria per i chiamanti che devono controllare o innalzare lo stato.
Innalzamento dello stato
L'innalzamento dello stato in Compose è un pattern di spostamento dello stato al chiamante di un elemento componibile per rendere l'elemento componibile 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 visualizzareonValueChange: (T) -> Unit: un evento che richiede la modifica del valore, doveTè il nuovo valore proposto
Tuttavia, non sei limitato a onValueChange. Se sono appropriati eventi più specifici per l'elemento componibile, devi definirli utilizzando le espressioni lambda.
Lo stato innalzato in questo modo ha alcune proprietà importanti:
- Single Source Of Truth: spostando lo stato anziché duplicarlo, ci assicuriamo che esista una sola fonte di verità. In questo modo si evitano bug.
- Incapsulato: solo gli elementi componibili stateful possono modificare il proprio stato. È completamente interno.
- Condivisibile: lo stato innalzato può essere condiviso con più elementi componibili. Se volessi leggere
namein un elemento componibile diverso, l'innalzamento ti consentirebbe di farlo. - Intercettabile: i chiamanti degli elementi componibili stateless possono decidere di ignorare o modificare gli eventi prima di modificare lo stato.
- Disaccoppiato: lo stato degli elementi componibili stateless può essere archiviato ovunque. Ad esempio, ora è possibile spostare
namein un oggettoViewModel.
Nell'esempio, estrai name e onValueChange da HelloContent e li sposti verso l'alto nell'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") }) } }
Innalzando lo stato da HelloContent, è più facile ragionare sull'elemento componibile, riutilizzarlo in situazioni diverse e testarlo. HelloContent è disaccoppiato dal modo in cui viene archiviato il relativo stato. Il disaccoppiamento significa che se modifichi o sostituisci HelloScreen, non devi modificare l'implementazione di HelloContent.
Il pattern in cui lo stato scende e gli eventi salgono è chiamato flusso di dati unidirezionale. In questo caso, lo stato scende da HelloScreen a HelloContent e gli eventi salgono da HelloContent a HelloScreen. Seguendo il flusso di dati unidirezionale, puoi disaccoppiare gli elementi componibili che mostrano lo stato nella UI dalle parti dell'app che archiviano e modificano lo stato.
Per saperne di più, visita la pagina Dove innalzare lo stato.
Ripristino dello stato in Compose
L'API rememberSaveable si comporta in modo simile a remember perché conserva lo stato tra le ricomposizioni e anche tra la ricreazione di attività o 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'
@Parcelize
annotazione all'oggetto. L'oggetto diventa serializzabile e può essere raggruppato. Ad esempio, questo codice crea un tipo di dati City serializzabile 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
L'innalzamento dello stato semplice può essere gestito nelle stesse funzioni componibili. Tuttavia, se la quantità di stato da tenere traccia aumenta o la logica da eseguire nelle funzioni componibili, è una buona pratica delegare le responsabilità di logica e stato ad altre classi: contenitori di stato.
Per saperne di più, consulta la documentazione sull'innalzamento dello stato in Compose o, più in generale, la pagina Contenitori di stato e stato della UI nella guida all'architettura.
Attivazione di nuovo dei calcoli di remember quando le chiavi cambiano
L'remember API viene spesso utilizzata insieme a MutableState:
var name by remember { mutableStateOf("") }
Qui, 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 il lambda calculation e ne memorizza il risultato. Durante la ricomposizione, 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 è costosa da inizializzare o calcolare. Potresti non voler ripetere questo calcolo in ogni ricomposizione.
Un esempio è la creazione di questo ShaderBrush oggetto, 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
si ricompone, remember invalida la cache ed esegue di nuovo il blcolo lambda di calcolo. Questo meccanismo ti consente di controllare la durata di un oggetto nella composizione. Il calcolo rimane valido finché gli input non cambiano, anziché finché il valore memorizzato non esce dalla composizione.
Gli esempi seguenti mostrano come funziona questo meccanismo.
In questo snippet, viene creato un oggetto ShaderBrush e utilizzato come pittura di sfondo
di un elemento componibile Box. remember memorizza l'istanza ShaderBrush
perché è costosa da ricreare, come spiegato in precedenza. remember accetta avatarRes come parametro key1, ovvero l'immagine di sfondo selezionata. Se 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 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 innalzato 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
rememberMyAppState funzione riceve windowSizeClass, che funge da
parametro key per remember. Se questo parametro cambia, l'app deve ricreare la classe di contenitore di stato semplice con l'ultimo valore. 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 è cambiata e invalidare il valore memorizzato.
consulta il post del blog Jetpack Compose - When should I use derivedStateOf?.Archiviazione dello stato con chiavi oltre la ricomposizione
L'API rememberSaveable è un wrapper intorno a remember che può archiviare i dati in un oggetto 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 degli input cambia. La volta successiva che la funzione si ricompone, rememberSaveable esegue di nuovo 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ù sullo stato e su Jetpack Compose, consulta le seguenti risorse aggiuntive.
Esempi
Codelab
Video
Blog
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Progettazione dell'interfaccia utente di Compose
- Salvataggio dello stato della UI in Compose
- Effetti collaterali in Compose