Utiliser l'état Compose en dehors de l'UI

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 .value d'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érification if ou un return anticipé), 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 MutableState distinctes et précises, et encapsuler les écritures multipropriétés dans Snapshot.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 seul StateFlow<UiState> monolithique (qui invalide chaque collecteur lorsqu'un champ change) et des propriétés StateFlow distinctes (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.
  • StateStore encapsulé dans un gestionnaire ou un dépôt
  • Clés implémentant PersistToDataStore et synchronisées à l'aide de syncToDataStore
É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.saveable pour l'état enregistré réactif : déléguer searchQuery à SavedStateHandle.saveable avec mutableStateOf (à partir de androidx.lifecycle:lifecycle-viewmodel-compose) conserve la valeur lors de l'arrêt du processus en tant que MutableState, de sorte que transform suit automatiquement les lectures. Évitez d'utiliser SavedStateHandle.saved seul, car il renvoie une propriété Kotlin simple plutôt qu'un State Compose et ne déclenche pas de recomposition en cas de modification.
  • Écritures atomiques multipropriétés avec Snapshot.withMutableSnapshot : la mise à jour de searchQuery et de selectedTaskIds dans Snapshot.withMutableSnapshot isole les écritures et valide les deux modifications dans une seule transaction d'instantané atomique. Ainsi, les observateurs (y compris transform) voient les deux mises à jour ensemble dans une seule passe de recomposition, au lieu d'observer le nouveau searchQuery associé à l'ancien selectedTaskIds.
  • Réutiliser des flux entre les recompositions avec remember(key) : encapsuler tasksRepository.getTasks(searchQuery) dans remember avec searchQuery comme clé garantit que lorsque selectedTaskIds change et recompose le bloc transform, Compose réutilise le flux de base de données Flow existant 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.