StateStore   Parte di Android Jetpack.

StateStore è un container con sicurezza dei tipi che archivia ed espone lo stato dell'applicazione condiviso come State di Compose nell'app.

Casi d'uso

Creando un StateStore con ambito applicazione, puoi condividere lo stato tra i componenti dell'applicazione all'interno dello stesso processo:

  • Più attività: se la tua app mostra più attività contemporaneamente (ad esempio un'attività di posta in arrivo e un'attività di editor in un client di posta) o ha un'attività che supporta multi-istanza (ad esempio un'attività di visualizzazione di documenti), puoi utilizzare StateStore per condividere informazioni tra di loro.
  • Attività e servizio: sincronizza lo stato tra un'attività e un CarAppService o un'AppFunction in background.
  • Attività e provider di contenuti o widget: condividi lo stato tra un'attività e un widget della schermata Home.

Se un singleton richiede solo State<T> in memoria senza ricerca con chiave o ripristino dell'interruzione del processo supportato da DataStore, puoi esporre le proprietà mutableStateOf direttamente sul singleton. Per un confronto delle API di stato nella tua app, consulta Scegli l'API di stato giusta per la tua architettura.

Panoramica dell'API

Il pacchetto androidx.appstate.statestore fornisce le classi StateStore e 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,
)

Definisci lo stato

Per garantire la sicurezza dei tipi durante l'archiviazione e il recupero dei valori, StateStore utilizza la classe StateStoreKey per definire le chiavi utilizzate per leggere e scrivere i valori. Mantieni le definizioni StateStoreKey private o internal nel gestore, nel repository o nel modulo proprietario di quella sezione dello stato:

@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)

Crea e inserisci istanze StateStore

StateStore è una classe normale di cui crei un'istanza una volta nell'ambito dell'applicazione o del modulo (ad esempio, nella classe Application o in un modulo di inserimento delle dipendenze) e che inserisci nei gestori o nei repository che leggono e scrivono lo stato, anziché esporre StateStore direttamente alle classi 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,
)

Stato di lettura e scrittura

All'interno del gestore o del repository, leggi, scrivi e aggiorna i valori nel StateStore inserito utilizzando le definizioni StateStoreKey private ed esponi le proprietà State<T> di sola lettura e le funzioni di mutazione ai chiamanti.

Stato della lettura

Utilizza getState(stateKey, defaultValue) per recuperare un oggetto State<T>. Se la chiave non è ancora inizializzata, StateStore la compila con il parametro defaultValue, che per impostazione predefinita è stateKey.defaultValue (oppure puoi passare un defaultValue personalizzato durante l'inizializzazione di una chiave non compilata). Poiché getState restituisce un State<T>, i ViewModel, i componibili dell'interfaccia utente, i widget e i blocchi transform possono leggere la proprietà del gestore in modo reattivo:

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}")
    }
}

Stato di scrittura

Utilizza setState(stateKey, value) all'interno del gestore per inizializzare o sostituire il valore associato a una chiave:

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)
    }
}

Aggiorna stato

Se devi aggiornare uno stato in base al suo valore corrente, utilizza updateState. In questo modo viene recuperato il valore corrente (o viene utilizzato quello predefinito se non è impostato) e viene applicata la funzione lambda di aggiornamento:

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
            }
        }
    }
}

Integrazioni

StateStore si integra con Jetpack DataStore per rendere persistente e ripristinare lo stato in caso di interruzione del processo e riavvio dell'app.

DataStore

L'artefatto androidx.appstate.statestore:statestore-datastore ti consente di configurare la persistenza una sola volta nell'istanza StateStore condivisa, in modo che le chiavi persistenti di qualsiasi gestore vengano salvate e ripristinate automaticamente dal disco utilizzando DataStore.

Il pacchetto androidx.appstate.datastore fornisce le seguenti API:

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. Implementa PersistToDataStore: contrassegna qualsiasi StateStoreKey che deve essere mantenuto implementando l'interfaccia PersistToDataStore:

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

  1. Associa StateStore a DataStore: chiama syncToDataStore una volta durante la configurazione dell'istanza StateStore condivisa (ad esempio, nella classe Application o nel modulo di inserimento delle dipendenze). Poiché syncToDataStore è una funzione suspend che si sospende a tempo indeterminato per ripristinare i valori salvati dal disco e continuare a salvare le modifiche future nel coroutineScope fornito, chiamala all'interno di launch e passa il CoroutineScope a livello di applicazione. All'avvio, getState restituisce inizialmente defaultValue in memoria e aggiorna State una volta che DataStore ha terminato la lettura dal disco:

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)
}

Linee guida per la persistenza di DataStore

Poiché syncToDataStore serializza e scrive su disco ogni volta che il valore di una chiave PersistToDataStore cambia, segui queste linee guida quando scegli cosa e quando rendere persistente:

  • Salva i timestamp di inizio o fine anziché ogni tick del timer: per i timer o l'avanzamento della riproduzione, mantieni un valore di riferimento stabile (ad esempio endTimeMillis o startedAtMillis) anziché aggiornare una chiave PersistToDataStore ogni secondo o fotogramma e calcola il tempo trascorso o rimanente in tempo reale nella UI.
  • Elimina l'input utente rapido prima di aggiornare una chiave persistente: per le bozze di testo, le query di ricerca o i cursori, mantieni l'input immediato in un MutableState in memoria (o in un StateStoreKey che non implementa PersistToDataStore) e chiama setState sul StateStoreKey persistente solo dopo che l'input si è stabilizzato.
  • Mantieni le chiavi persistenti leggere: tutte le chiavi PersistToDataStore in un StateStore vengono serializzate insieme in un unico file a ogni aggiornamento. Mantieni lo stato della sessione compatto, ad esempio ID attivi, selezioni di filtri o bozze di moduli in corso, e conserva raccolte di grandi dimensioni o set di dati interrogabili in Room.