State<T> et MutableState<T> de Jetpack Compose sont des conteneurs de valeurs à usage général, compatibles avec le système d'instantanés Compose. Bien qu'ils soient le plus souvent utilisés pour piloter la recomposition de l'UI, le système d'instantanés lui-même n'est pas lié au rendu de l'UI.
Avec les bibliothèques AppState (Transform et StateStore), vous pouvez conserver, dériver, observer et rendre persistant l'State Compose dans les ViewModels, les gestionnaires, les dépôts et les services d'arrière-plan.
Pourquoi utiliser l'état Compose au-delà de l'UI ?
Lorsque vous lisez State Compose dans un bloc d'observation d'état, tel qu'un composable d'UI, transform ou derivedStateOf, l'environnement d'exécution Compose offre deux avantages clés pour toutes les couches de votre architecture :
- Suivi automatique et dynamique des dépendances : la lecture de la
.valued'un état (ou l'utilisation de la délégation de propriétéby) enregistre automatiquement cet état en tant que dépendance du bloc. Comme le suivi est dynamique, les dépendances sont déterminées au moment de l'exécution par les chemins de code qui s'exécutent. Si une branche conditionnelle n'est pas empruntée (par exemple, après une vérificationifou unreturnanticipé), les états de cette branche ne sont pas lus et les modifications apportées à ces états ne déclenchent pas de recalculs inutiles. - Cohérence et précision : vous pouvez déclarer des propriétés
MutableStatedistinctes et précises, et encapsuler les écritures multipropriétés dansSnapshot.withMutableSnapshot. Les mises à jour intermédiaires sont isolées dans l'instantané modifiable et appliquées de manière atomique lorsque le bloc se termine. Ainsi, les observateurs qui lisent un instantané ne voient jamais d'état intermédiaire incohérent et ne recalculent que lorsque les propriétés spécifiques qu'ils lisent changent. Contrairement àStateFlow, vous n'avez pas à choisir entre un seulStateFlow<UiState>monolithique (qui invalide chaque collecteur lorsqu'un champ change) et des propriétésStateFlowdistinctes (qui ne peuvent pas être mises à jour ensemble dans une seule transaction atomique).
Choisir l'API d'état appropriée pour votre architecture
Choisissez comment conserver et rendre persistant l'State Compose en fonction de la durée de vie d'un élément d'état et des composants qui doivent y accéder :
| Catégorie d'État | Durée de vie et portée | Exemples | API Compose State recommandées |
|---|---|---|---|
| En cours | Toujours stocké en mémoire, interrogé à nouveau au démarrage et non conservé après l'arrêt du processus. | État de la connexion réseau, état de la connexion Bluetooth, état temporaire de l'interface utilisateur (sélection d'un élément, par exemple). |
|
| État temporaire de l'UI | Conserver après les modifications de configuration et l'arrêt du processus initié par le système, non stocké indéfiniment, limité à un seul écran ou une seule activité. | Éléments d'interface utilisateur développés ou réduits, position de défilement, requête de recherche à l'écran ou boutons d'activation/de désactivation des filtres. |
|
| État de session temporaire | Survit à l'arrêt du processus en arrière-plan, n'est pas stocké indéfiniment, partagé entre les activités, les services ou les widgets. | Minuteur de concentration actif, panier de paiement en plusieurs étapes ou brouillon de formulaire en cours. |
|
| État permanent | Conservés indéfiniment sur le disque. | Préférences et paramètres utilisateur (DataStore), données de l'application structurées et bibliothèques multimédias (Room). | Flux DataStore ou Room collectés dans transform à l'aide de collectAsState |
Utiliser l'état Compose dans un ViewModel
Dans un ViewModel, vous pouvez déclarer des entrées d'écran indépendantes en tant que propriétés MutableState précises, les conserver lors de l'arrêt du processus avec SavedStateHandle.saveable, mettre à jour plusieurs propriétés de manière atomique avec Snapshot.withMutableSnapshot et les combiner avec des flux de dépôt Flow dans 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, ) } }
Modèles clés dans ce ViewModel :
- Utiliser
SavedStateHandle.saveablepour l'état enregistré réactif : déléguersearchQueryàSavedStateHandle.saveableavecmutableStateOf(à partir deandroidx.lifecycle:lifecycle-viewmodel-compose) conserve la valeur lors de l'arrêt du processus en tant queMutableState, de sorte quetransformsuit automatiquement les lectures. Évitez d'utiliserSavedStateHandle.savedseul, car il renvoie une propriété Kotlin simple plutôt qu'unStateCompose et ne déclenche pas de recomposition en cas de modification. - Écritures atomiques multipropriétés avec
Snapshot.withMutableSnapshot: la mise à jour desearchQueryet deselectedTaskIdsdansSnapshot.withMutableSnapshotisole les écritures et valide les deux modifications dans une seule transaction d'instantané atomique. Ainsi, les observateurs (y compristransform) voient les deux mises à jour ensemble dans une seule passe de recomposition, au lieu d'observer le nouveausearchQueryassocié à l'ancienselectedTaskIds. - Réutiliser des flux entre les recompositions avec
remember(key): encapsulertasksRepository.getTasks(searchQuery)dansrememberavecsearchQuerycomme clé garantit que lorsqueselectedTaskIdschange et recompose le bloctransform, Compose réutilise le flux de base de donnéesFlowexistant au lieu de redémarrer la requête.
Partager l'état du système en direct dans votre application
Pour l'état du système en temps réel partagé entre plusieurs ViewModels, activités ou services (comme l'état de la connexion réseau ou Bluetooth), vous ne devez pas conserver l'état obsolète sur le disque en cas d'arrêt du processus. Interrogez plutôt l'état actuel du système au démarrage, conservez-le dans une propriété mutableStateOf privée au sein d'un singleton limité à l'application et exposez un State<T> en lecture seule :
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 accepte les lectures et les mises à jour cohérentes à partir de n'importe quel thread. NetworkCallback peut donc mettre à jour _isOnline.value directement à partir de son thread de rappel en arrière-plan. Si un rappel en arrière-plan met à jour plusieurs propriétés MutableState en même temps, encapsulez les écritures dans Snapshot.withMutableSnapshot pour qu'elles soient appliquées de manière atomique.
N'importe quel composant de votre application peut lire networkMonitor.isOnline.value, que ce soit pour une vérification ponctuelle, dans un composable d'UI ou dans un bloc d'observation d'état tel que listener, derivedStateOf ou transform. Par exemple, TaskListViewModel peut lire networkMonitor.isOnline.value dans son bloc 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, ) }
Étant donné que transform suit networkMonitor.isOnline.value ainsi que searchQuery, selectedTaskIds et le dépôt Flow, toute modification de la connectivité recalcule automatiquement TaskListState.Success sans interroger à nouveau la base de données.