Utilizza lo stato di Compose al di fuori della UI

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 .value di 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 controllo if o un return anticipato), gli stati all'interno di quel ramo non vengono letti e le modifiche non attivano ricalcoli non necessari.
  • Coerenza e granularità: puoi dichiarare proprietà MutableState separate e granulari e aggregare le scritture di più proprietà in Snapshot.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 di StateFlow, non devi scegliere tra un singolo StateFlow<UiState> monolitico (che invalida ogni raccoglitore quando un campo cambia) e proprietà StateFlow separate (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.saveable per lo stato salvato reattivo: la delega di searchQuery a SavedStateHandle.saveable con mutableStateOf (da androidx.lifecycle:lifecycle-viewmodel-compose) mantiene il valore durante l'interruzione del processo come MutableState, in modo che transform monitori automaticamente le letture. Evita di utilizzare SavedStateHandle.saved senza modificatori, che restituisce una proprietà Kotlin semplice anziché State di Compose e non attiva la ricomposizione in caso di modifica.
  • Scritture atomiche di più proprietà con Snapshot.withMutableSnapshot: l'aggiornamento di searchQuery e selectedTaskIds all'interno di Snapshot.withMutableSnapshot isola le scritture ed esegue il commit di entrambe le modifiche in una singola transazione di snapshot atomica, in modo che gli osservatori (incluso transform) vedano entrambi gli aggiornamenti insieme in un unico passaggio di ricomposizione, anziché osservare il nuovo searchQuery associato a selectedTaskIds non aggiornato.
  • Riutilizza i flussi tra le ricomposizioni con remember(key): l'aggregazione di tasksRepository.getTasks(searchQuery) in remember con searchQuery come chiave garantisce che quando selectedTaskIds cambia e ricompone il blocco transform, Compose riutilizza il Flow del 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.