StateStore   Fait partie d'Android Jetpack.

StateStore est un conteneur de sûreté du typage qui stocke et expose l'état de l'application partagée en tant que State Compose dans l'ensemble de votre application.

Cas d'utilisation

En créant un StateStore à l'échelle de l'application, vous pouvez partager l'état entre les composants d'application au sein du même processus :

  • Activités multiples : si votre application affiche plusieurs activités simultanément (par exemple, une activité de boîte de réception et une activité d'éditeur dans un client de messagerie) ou si elle comporte une activité compatible avec les multi-instances (par exemple, une activité de visionneuse de documents), vous pouvez utiliser StateStore pour partager des informations entre elles.
  • Activité et service : synchronisez l'état entre une activité et un CarAppService ou une AppFunction en arrière-plan.
  • Activité et fournisseur de contenu ou widget : partagez l'état entre une activité et un widget de l'écran d'accueil.

Si un singleton n'a besoin que de State<T> en mémoire sans recherche par clé ni restauration de l'arrêt du processus basée sur DataStore, vous pouvez exposer les propriétés mutableStateOf directement sur le singleton. Pour comparer les API d'état dans votre application, consultez Choisir l'API d'état adaptée à votre architecture.

Présentation de l'API

Le package androidx.appstate.statestore fournit les classes StateStore et StateStoreKey :

package androidx.appstate.statestore

import androidx.compose.runtime.State
import kotlinx.serialization.Serializable

public class StateStore {
    public val keys: Set<StateStoreKey<*>>
    public fun <T> getState(
        stateKey: StateStoreKey<T>,
        defaultValue: T = stateKey.defaultValue,
    ): State<T>
    public fun <T> setState(stateKey: StateStoreKey<T>, value: T)
    public inline fun <T> updateState(
        stateKey: StateStoreKey<T>,
        defaultValue: T = stateKey.defaultValue,
        update: (T) -> T,
    )
}

@Serializable
public open class StateStoreKey<T>(
    public val defaultValue: T,
)

Définir votre état

Pour garantir la sûreté du typage lors du stockage et de la récupération des valeurs, StateStore utilise la classe StateStoreKey pour définir les clés utilisées pour lire et écrire des valeurs. Conservez les définitions StateStoreKey private ou internal dans le gestionnaire, le dépôt ou le module propriétaire de cette partie de l'état :

@Serializable
sealed interface FocusSession {
    @Serializable
    data object Idle : FocusSession

    @Serializable
    data class Running(
        val taskId: String,
        val endTimeMillis: Long,
    ) : FocusSession

    @Serializable
    data class Paused(
        val taskId: String,
        val remainingMillis: Long,
    ) : FocusSession
}

@Serializable
private data object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)

Créer et injecter des instances StateStore

StateStore est une classe standard que vous instanciez une fois au niveau du champ d'application ou du module (par exemple, dans votre classe Application ou un module d'injection de dépendances) et que vous injectez dans les gestionnaires ou les dépôts qui lisent et écrivent l'état, plutôt que d'exposer StateStore directement aux classes d'UI :

// Instantiate at the application or DI module level
val stateStore = StateStore()

// Inject into managers or repositories that encapsulate their own keys
class FocusSessionManager(
    private val stateStore: StateStore,
)

État de lecture et d'écriture

Dans votre gestionnaire ou votre dépôt, lisez, écrivez et mettez à jour les valeurs dans le StateStore injecté à l'aide de vos définitions StateStoreKey privées, et exposez les propriétés State<T> en lecture seule et les fonctions de mutation aux appelants.

État de lecture

Utilisez getState(stateKey, defaultValue) pour récupérer un objet State<T>. Si la clé n'est pas encore initialisée, StateStore la remplit avec le paramètre defaultValue, qui est défini par défaut sur stateKey.defaultValue (vous pouvez également transmettre un defaultValue personnalisé lors de l'initialisation d'une clé non renseignée). Comme getState renvoie un State<T>, les ViewModels, les composables d'UI, les widgets et les blocs transform peuvent lire la propriété du gestionnaire de manière réactive :

class FocusSessionManager(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
}

class FocusTimerViewModel(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}

@Composable
fun FocusBanner(viewModel: FocusTimerViewModel = viewModel()) {
    // Reads state reactively; recomposes when FocusSessionKey updates
    when (val session = viewModel.session.value) {
        FocusSession.Idle -> Text(text = "No active focus session")
        is FocusSession.Running -> Text(text = "Focusing on ${session.taskId}")
        is FocusSession.Paused -> Text(text = "Paused: ${session.taskId}")
    }
}

État d'écriture

Utilisez setState(stateKey, value) dans votre gestionnaire pour initialiser ou remplacer la valeur associée à une clé :

class FocusSessionManager(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)

    fun startFocus(taskId: String, endTimeMillis: Long) {
        stateStore.setState(
            FocusSessionKey,
            FocusSession.Running(
                taskId = taskId,
                endTimeMillis = endTimeMillis,
            ),
        )
    }

    fun endFocus() {
        stateStore.setState(FocusSessionKey, FocusSession.Idle)
    }
}

Modifier l'état

Si vous devez mettre à jour un état en fonction de sa valeur actuelle, utilisez updateState. Cette opération récupère la valeur actuelle (ou utilise la valeur par défaut si elle n'est pas définie) et applique votre lambda de mise à jour :

class FocusSessionManager(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)

    fun pauseFocus(nowMillis: Long) {
        stateStore.updateState(FocusSessionKey) { current ->
            if (current is FocusSession.Running) {
                FocusSession.Paused(
                    taskId = current.taskId,
                    remainingMillis = (current.endTimeMillis - nowMillis)
                        .coerceAtLeast(0L),
                )
            } else {
                current
            }
        }
    }

    fun resumeFocus(nowMillis: Long) {
        stateStore.updateState(FocusSessionKey) { current ->
            if (current is FocusSession.Paused) {
                FocusSession.Running(
                    taskId = current.taskId,
                    endTimeMillis = nowMillis + current.remainingMillis,
                )
            } else {
                current
            }
        }
    }
}

Intégrations

StateStore s'intègre à Jetpack DataStore pour conserver et restaurer l'état en cas d'arrêt du processus et de redémarrage de l'application.

DataStore

L'artefact androidx.appstate.statestore:statestore-datastore vous permet de configurer la persistance une seule fois sur votre instance StateStore partagée, de sorte que les clés persistantes de n'importe quel gestionnaire soient automatiquement enregistrées et restaurées à partir du disque à l'aide de DataStore.

Le package androidx.appstate.datastore fournit les API suivantes :

package androidx.appstate.datastore

import androidx.appstate.statestore.StateStore
import androidx.datastore.core.DataStore
import kotlinx.coroutines.CoroutineScope

public interface PersistToDataStore

public suspend fun StateStore.syncToDataStore(
    path: String,
    coroutineScope: CoroutineScope,
)

public suspend fun StateStore.syncToDataStore(
    dataStore: DataStore<StateStorePreferences>,
)
  1. Implémenter PersistToDataStore : marquez tout StateStoreKey qui doit être conservé en implémentant l'interface PersistToDataStore :

@Serializable
private data object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
    PersistToDataStore

  1. Lier StateStore à DataStore : appelez syncToDataStore une fois lors de la configuration de votre instance StateStore partagée (par exemple, dans votre classe Application ou votre module d'injection de dépendances). Étant donné que syncToDataStore est une fonction suspend qui se suspend indéfiniment pour restaurer les valeurs enregistrées à partir du disque et continuer à enregistrer les modifications futures dans le coroutineScope fourni, appelez-la dans launch et transmettez votre CoroutineScope au niveau de l'application. Au démarrage, getState renvoie initialement defaultValue en mémoire et met à jour State une fois que DataStore a fini de lire le disque :

class ProductivityApplication : Application() {
    val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    val stateStore = StateStore().also { store ->
        // Restore saved values and automatically persist future changes
        // for all PersistToDataStore keys across managers
        applicationScope.launch {
            store.syncToDataStore(
                path = dataStoreFile("statestore.json").absolutePath,
                coroutineScope = applicationScope,
            )
        }
    }

    val focusSessionManager = FocusSessionManager(stateStore)
}

Consignes de persistance DataStore

Étant donné que syncToDataStore sérialise et écrit sur le disque chaque fois que la valeur d'une clé PersistToDataStore change, suivez ces consignes pour choisir ce que vous souhaitez conserver et quand :

  • Enregistrez les codes temporels de début ou de fin au lieu de chaque graduation du minuteur : pour les minuteurs ou la progression de la lecture, conservez une valeur de référence stable (comme endTimeMillis ou startedAtMillis) plutôt que de mettre à jour une clé PersistToDataStore chaque seconde ou chaque frame, et calculez l'affichage en direct du temps écoulé ou restant dans l'UI.
  • Évitez les entrées utilisateur rapides avant de mettre à jour une clé persistante : pour les brouillons de texte, les requêtes de recherche ou les curseurs, conservez l'entrée immédiate dans un MutableState en mémoire (ou un StateStoreKey qui n'implémente pas PersistToDataStore) et n'appelez setState sur le StateStoreKey persistant qu'une fois l'entrée stabilisée.
  • Limitez la taille des clés persistantes : toutes les clés PersistToDataStore d'un StateStore sont sérialisées ensemble dans un seul fichier à chaque mise à jour. Conservez l'état de session compact (par exemple, les ID actifs, les sélections de filtres ou les brouillons de formulaires en cours) et conservez les collections volumineuses ou les ensembles de données interrogeables dans Room.