StateStore   Teil von Android Jetpack.

„StateStore“ ist ein typsicherer Container, der den freigegebenen Anwendungsstatus als Compose-State in Ihrer App speichert und verfügbar macht.

Anwendungsfälle

Wenn Sie einen StateStore mit Anwendungsbereich erstellen, können Sie den Status zwischen Anwendungskomponenten innerhalb desselben Prozesses teilen:

  • Mehrere Aktivitäten: Wenn in deiner App mehrere Aktivitäten gleichzeitig angezeigt werden (z. B. eine Posteingangsaktivität und eine Editoraktivität in einem E-Mail-Client) oder eine Aktivität vorhanden ist, die Multi-Instanz-Modus unterstützt (z. B. eine Dokumentanzeigeaktivität), kannst du StateStore verwenden, um Informationen zwischen ihnen zu teilen.
  • Aktivität und Dienst: Synchronisieren Sie den Status zwischen einer Aktivität und einem Hintergrund-CarAppService oder einer AppFunction.
  • Aktivität und Contentanbieter oder Widget: Status zwischen einer Aktivität und einem Startbildschirm-Widget teilen.

Wenn ein Singleton nur State<T> im Arbeitsspeicher benötigt, ohne dass eine schlüsselbasierte Suche oder eine DataStore-gestützte Wiederherstellung nach einer Prozessbeendigung erforderlich ist, können Sie stattdessen mutableStateOf-Properties direkt im Singleton verfügbar machen. Einen Vergleich der Status-APIs in Ihrer App finden Sie unter Die richtige Status-API für Ihre Architektur auswählen.

API-Übersicht

Das androidx.appstate.statestore-Paket bietet die Klassen StateStore und 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,
)

Bundesland angeben

Um die Typsicherheit beim Speichern und Abrufen von Werten zu gewährleisten, verwendet StateStore die Klasse StateStoreKey, um die Schlüssel zum Lesen und Schreiben von Werten zu definieren. Beschränken Sie StateStoreKey-Definitionen (private oder internal) auf den Manager, das Repository oder das Modul, das diesen Teil des Status besitzt:

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

StateStore-Instanzen erstellen und einfügen

StateStore ist eine reguläre Klasse, die Sie einmal im Anwendungs- oder Modulbereich instanziieren (z. B. in Ihrer Application-Klasse oder einem Dependency-Injection-Modul) und in die Manager oder Repositories einfügen, die den Status lesen und schreiben, anstatt StateStore direkt für UI-Klassen freizugeben:

// 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,
)

Lese- und Schreibstatus

In Ihrem Manager oder Repository können Sie Werte in der eingefügten StateStore mit Ihren privaten StateStoreKey-Definitionen lesen, schreiben und aktualisieren und Lesezugriff-State<T>-Eigenschaften und Mutationsfunktionen für Aufrufer verfügbar machen.

Lesestatus

Verwenden Sie getState(stateKey, defaultValue), um ein State<T>-Objekt abzurufen. Wenn der Schlüssel noch nicht initialisiert ist, wird er von StateStore mit dem Parameter defaultValue gefüllt, der standardmäßig auf stateKey.defaultValue gesetzt ist. Sie können aber auch einen benutzerdefinierten defaultValue übergeben, wenn Sie einen nicht gefüllten Schlüssel initialisieren. Da getState ein State<T> zurückgibt, können ViewModels, UI-Kompositionen, Widgets und transform-Blöcke die Property des Managers reaktiv lesen:

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

Schreibstatus

Verwenden Sie setState(stateKey, value) in Ihrem Manager, um den mit einem Schlüssel verknüpften Wert zu initialisieren oder zu ersetzen:

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

Status aktualisieren

Wenn Sie einen Status basierend auf seinem aktuellen Wert aktualisieren müssen, verwenden Sie updateState. Dadurch wird der aktuelle Wert abgerufen (oder der Standardwert verwendet, falls nicht festgelegt) und die Update-Lambda-Funktion angewendet:

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

Integrationen

StateStore lässt sich in Jetpack DataStore einbinden, um den Status bei Prozessbeendigung und App-Neustarts beizubehalten und wiederherzustellen.

Datastore

Mit dem Artefakt androidx.appstate.statestore:statestore-datastore können Sie die Persistenz einmalig auf Ihrer freigegebenen StateStore-Instanz konfigurieren, sodass die persistenten Schlüssel aller Manager automatisch mit DataStore auf dem Laufwerk gespeichert und von dort wiederhergestellt werden.

Das androidx.appstate.datastore-Paket bietet folgende APIs:

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. PersistToDataStore implementieren: Markieren Sie alle StateStoreKey, die beibehalten werden sollen, indem Sie die Schnittstelle PersistToDataStore implementieren:

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

  1. StateStore an DataStore binden: Rufen Sie syncToDataStore einmal auf, wenn Sie Ihre freigegebene StateStore-Instanz einrichten (z. B. in Ihrer Application-Klasse oder Ihrem Dependency-Injection-Modul). Da syncToDataStore eine suspend-Funktion ist, die unbegrenzt angehalten wird, um gespeicherte Werte vom Laufwerk wiederherzustellen und zukünftige Änderungen im angegebenen coroutineScope zu speichern, rufen Sie sie innerhalb von launch auf und übergeben Sie Ihre CoroutineScope auf Anwendungsebene. Beim Start gibt getState zuerst defaultValue im Arbeitsspeicher zurück und aktualisiert State, sobald DataStore das Lesen vom Laufwerk abgeschlossen hat:

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

Richtlinien für die DataStore-Persistenz

Da syncToDataStore serialisiert und auf das Laufwerk schreibt, wenn sich der Wert eines PersistToDataStore-Schlüssels ändert, sollten Sie die folgenden Richtlinien beachten, wenn Sie entscheiden, was und wann Sie persistent speichern möchten:

  • Start- oder Endzeitstempel anstelle jedes Timer-Ticks speichern: Speichere für Timer oder den Wiedergabefortschritt einen stabilen Referenzwert (z. B. endTimeMillis oder startedAtMillis), anstatt einen PersistToDataStore-Schlüssel jede Sekunde oder jeden Frame zu aktualisieren, und berechne die verstrichene oder verbleibende Zeit in der Benutzeroberfläche.
  • Schnelle Nutzereingaben vor dem Aktualisieren eines beibehaltenen Schlüssels entprellen: Bei Textentwürfen, Suchanfragen oder Schiebereglern solltest du die unmittelbare Eingabe in einem speicherinternen MutableState (oder einem StateStoreKey, der PersistToDataStore nicht implementiert) speichern und setState erst dann für den beibehaltenen StateStoreKey aufrufen, wenn die Eingabe abgeschlossen ist.
  • Persistente Schlüssel klein halten: Alle PersistToDataStore-Schlüssel in einem StateStore werden bei jeder Aktualisierung zusammen in einer einzigen Datei serialisiert. Speichern Sie kompakte Sitzungsstatus wie aktive IDs, Filterauswahlen oder laufende Formular-Entwürfe und speichern Sie große Sammlungen oder abfragbare Datasets in Room.