State<T> e MutableState<T> di Jetpack Compose sono contenitori di valori per uso generico supportati dal sistema di snapshot di Compose. Sebbene vengano utilizzati più comunemente per promuovere la ricomposizione della UI, il sistema di snapshot in sé non è legato al rendering della UI.
Con le librerie AppState, Transform
e StateStore, puoi conservare, derivare, osservare e rendere persistente
Compose State in ViewModel, gestori, repository e servizi
in background.
Perché utilizzare Compose State oltre la UI?
Quando leggi Compose State all'interno di un blocco di osservazione dello stato, ad esempio un composable dell'interfaccia utente, transform o derivedStateOf, il runtime di Compose offre due vantaggi chiave in qualsiasi livello dell'architettura:
- Monitoraggio automatico e dinamico delle dipendenze: la lettura del
.valuedi uno stato (o l'utilizzo della delega delle proprietàby) registra automaticamente lo stato come dipendenza del blocco. Poiché il monitoraggio è dinamico, le dipendenze vengono determinate in fase di runtime in base ai percorsi del codice eseguiti. Se un ramo condizionale non viene eseguito (ad esempio, dopo un controlloifo unreturnanticipato), gli stati all'interno di quel ramo non vengono letti e le modifiche non attivano ricalcoli non necessari. - Coerenza e granularità: puoi dichiarare proprietà
MutableStateseparate e granulari e aggregare le scritture di più proprietà inSnapshot.withMutableSnapshot. Gli aggiornamenti intermedi sono isolati nello snapshot modificabile e applicati in modo atomico al completamento del blocco, in modo che gli osservatori che leggono all'interno di uno snapshot non vedano mai uno stato intermedio incoerente e ricalcolino solo quando cambiano le proprietà specifiche che leggono. A differenza diStateFlow, non devi scegliere tra un singoloStateFlow<UiState>monolitico (che invalida ogni raccoglitore quando un campo cambia) e proprietàStateFlowseparate (che non possono essere aggiornate insieme in una singola transazione atomica).
Scegli l'API di stato giusta per la tua architettura
Scegli come conservare e rendere persistente State di Compose in base alla durata di un elemento dello stato e ai componenti che devono accedervi:
| Categoria stato | Durata e ambito | Esempi | API Compose State consigliate |
|---|---|---|---|
| Stato attivo | Sempre archiviati in memoria; vengono interrogati all'avvio e non vengono mantenuti dopo l'interruzione del processo. | Stato della connessione di rete, stato della connessione Bluetooth, stato temporaneo della UI, ad esempio la selezione degli elementi. |
|
| Stato dell'interfaccia utente temporaneo | Sopravvive alle modifiche alla configurazione e all'interruzione del processo avviata dal sistema, non viene archiviato a tempo indeterminato; limitato a una singola schermata o attività. | Elementi UI espansi o compressi, posizione di scorrimento, query di ricerca sullo schermo o pulsanti di attivazione/disattivazione dei filtri. |
|
| Stato della sessione temporaneo | Sopravvive all'interruzione del processo in background, non viene archiviato a tempo indeterminato; condiviso tra attività, servizi o widget. | Timer della modalità Niente distrazioni attivo, carrello con pagamento in più passaggi o bozza di modulo in corso. |
|
| Stato permanente | Conservati su disco a tempo indeterminato. | Preferenze e impostazioni utente (DataStore), dati dell'app strutturati e raccolte multimediali (Room). | Flussi DataStore o Room raccolti all'interno di transform utilizzando collectAsState |
Utilizza lo stato di Compose in un ViewModel
All'interno di un ViewModel, puoi dichiarare input indipendenti dello schermo come proprietà MutableState granulari, renderli persistenti anche dopo l'interruzione del processo con SavedStateHandle.saveable, aggiornare più proprietà in modo atomico con Snapshot.withMutableSnapshot e combinarle con i flussi Flow del repository all'interno di un transform:
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
Pattern chiave in questo ViewModel:
- Utilizza
SavedStateHandle.saveableper lo stato salvato reattivo: la delega disearchQueryaSavedStateHandle.saveableconmutableStateOf(daandroidx.lifecycle:lifecycle-viewmodel-compose) mantiene il valore durante l'interruzione del processo comeMutableState, in modo chetransformmonitori automaticamente le letture. Evita di utilizzareSavedStateHandle.savedsenza modificatori, che restituisce una proprietà Kotlin semplice anzichéStatedi Compose e non attiva la ricomposizione in caso di modifica. - Scritture atomiche di più proprietà con
Snapshot.withMutableSnapshot: l'aggiornamento disearchQueryeselectedTaskIdsall'interno diSnapshot.withMutableSnapshotisola le scritture ed esegue il commit di entrambe le modifiche in una singola transazione di snapshot atomica, in modo che gli osservatori (inclusotransform) vedano entrambi gli aggiornamenti insieme in un unico passaggio di ricomposizione, anziché osservare il nuovosearchQueryassociato aselectedTaskIdsnon aggiornato. - Riutilizza i flussi tra le ricomposizioni con
remember(key): l'aggregazione ditasksRepository.getTasks(searchQuery)inrememberconsearchQuerycome chiave garantisce che quandoselectedTaskIdscambia e ricompone il bloccotransform, Compose riutilizza ilFlowdel database esistente anziché riavviare la query.
Condividi lo stato del sistema in tempo reale nell'app
Per lo stato del sistema in tempo reale condiviso tra più ViewModel, attività o
servizi, ad esempio lo stato della connessione di rete o Bluetooth, non è consigliabile
mantenere lo stato obsoleto sul disco durante l'interruzione del processo. Esegui invece una query sullo stato attuale del sistema all'avvio, conservalo in una proprietà mutableStateOf privata all'interno di un singleton con ambito applicazione ed esponi un State<T> di sola lettura:
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState supporta letture e aggiornamenti coerenti da qualsiasi thread, quindi
NetworkCallback può aggiornare _isOnline.value direttamente dal suo thread di callback
in background. Se un callback in background aggiorna più proprietà MutableState contemporaneamente, inserisci le scritture in Snapshot.withMutableSnapshot in modo che vengano applicate in modo atomico.
Qualsiasi componente della tua app può leggere networkMonitor.isOnline.value, sia come controllo una tantum, sia all'interno di un composable dell'interfaccia utente o di un blocco di osservazione dello stato come listener, derivedStateOf o transform. Ad esempio, TaskListViewModel può leggere networkMonitor.isOnline.value all'interno del suo blocco transform:
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
Poiché transform monitora networkMonitor.isOnline.value insieme a searchQuery, selectedTaskIds e al repository Flow, qualsiasi modifica alla connettività ricalcola automaticamente TaskListState.Success senza eseguire nuovamente una query sul database.