StateStore   Część Androida Jetpack.

StateStore to bezpieczny pod względem typów kontener, który przechowuje i udostępnia stan aplikacji jako State w całej aplikacji.

Przypadki użycia

Tworząc obiekt StateStore o zakresie aplikacji, możesz udostępniać stan między komponentami aplikacji w ramach tego samego procesu:

  • Wiele aktywności: jeśli aplikacja wyświetla jednocześnie wiele aktywności (np. aktywność skrzynki odbiorczej i aktywność edytora w kliencie poczty e-mail) lub ma aktywność, która obsługuje tryb podzielony (np. aktywność przeglądarki dokumentów), możesz użyć StateStore do udostępniania informacji między nimi.
  • Aktywność i usługa: synchronizuj stan między aktywnością a CarAppService lub AppFunction w tle.
  • Aktywność i mechanizm dostarczania treści lub widżet: udostępnianie stanu między aktywnością a widżetem na ekranie głównym.

Jeśli singleton potrzebuje tylko obiektu State<T> w pamięci bez wyszukiwania z kluczem lub przywracania śmierci procesu z obsługą DataStore, możesz zamiast tego udostępnić właściwości mutableStateOf bezpośrednio w singletonie. Porównanie interfejsów API stanu w aplikacji znajdziesz w artykule Wybieranie odpowiedniego interfejsu API stanu w architekturze.

Przegląd interfejsów API

Pakiet androidx.appstate.statestore udostępnia klasy StateStore i 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,
)

Zdefiniuj stan

Aby zapewnić bezpieczeństwo typów podczas przechowywania i pobierania wartości, StateStore używa klasy StateStoreKey do definiowania kluczy używanych do odczytu i zapisu wartości. Definicje StateStoreKey private lub internal zachowaj w menedżerze, repozytorium lub module, który jest właścicielem tego fragmentu stanu:

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

Tworzenie i wstrzykiwanie instancji StateStore

StateStore to zwykła klasa, której instancję tworzysz raz w zakresie aplikacji lub modułu (np. w klasie Application lub module wstrzykiwania zależności) i wstrzykujesz do menedżerów lub repozytoriów, które odczytują i zapisują stan, zamiast udostępniać StateStore bezpośrednio klasom interfejsu:

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

Stan odczytu i zapisu

W menedżerze lub repozytorium odczytuj, zapisuj i aktualizuj wartości we wstrzykniętym obiekcie StateStore, używając prywatnych definicji StateStoreKey, i udostępniaj wywołującym właściwości State<T> tylko do odczytu oraz funkcje mutacji.

Stan przeczytano / nie przeczytano

Użyj getState(stateKey, defaultValue), aby pobrać obiekt State<T>. Jeśli klucz nie został jeszcze zainicjowany, StateStore wypełnia go parametrem defaultValue, który domyślnie ma wartość stateKey.defaultValue (możesz też przekazać niestandardowy parametr defaultValue podczas inicjowania niewypełnionego klucza). Ponieważ getState zwraca State<T>, modele widoku, elementy interfejsu, widżety i bloki transform mogą odczytywać właściwości menedżera w sposób reaktywny:

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

Stan zapisu

Użyj setState(stateKey, value) w menedżerze, aby zainicjować lub zastąpić wartość powiązaną z kluczem:

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

Aktualizacja stanu

Jeśli chcesz zaktualizować stan na podstawie jego bieżącej wartości, użyj updateState. Pobiera bieżącą wartość (lub używa domyślnej, jeśli nie jest ustawiona) i stosuje funkcję lambda aktualizacji:

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

Integracje

StateStore integruje się z Jetpack DataStore, aby zachowywać i przywracać stan po śmierci procesu i ponownym uruchomieniu aplikacji.

DataStore

Artefakt androidx.appstate.statestore:statestore-datastore umożliwia jednorazowe skonfigurowanie trwałości w udostępnionej instancji StateStore, dzięki czemu klucze trwałe dowolnego menedżera są automatycznie zapisywane i przywracane z dysku za pomocą magazynu danych.

Pakiet androidx.appstate.datastore udostępnia te interfejsy 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. Zaimplementuj interfejs PersistToDataStore: oznacz wszystkie obiekty StateStoreKey, które powinny być utrwalane, implementując interfejs PersistToDataStore:

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

  1. Powiąż StateStore z DataStore: wywołaj syncToDataStore raz podczas konfigurowania udostępnionej instancji StateStore (np. w klasie Application lub module wstrzykiwania zależności). syncToDataStore to funkcja suspend, która zawiesza się na czas nieokreślony, aby przywrócić zapisane wartości z dysku i kontynuować zapisywanie przyszłych zmian w podanym coroutineScope. Wywołaj ją wewnątrz launch i przekaż CoroutineScope na poziomie aplikacji. Po uruchomieniu getState początkowo zwraca defaultValue w pamięci i aktualizuje State, gdy DataStore zakończy odczyt z dysku:

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

Wytyczne dotyczące trwałości DataStore

syncToDataStore serializuje i zapisuje dane na dysku za każdym razem, gdy zmienia się wartość klucza PersistToDataStore. Dlatego podczas wybierania danych do utrwalenia i momentu ich utrwalenia postępuj zgodnie z tymi wskazówkami:

  • Zapisuj sygnatury czasowe rozpoczęcia lub zakończenia zamiast każdego tyknięcia licznika: w przypadku liczników czasu lub postępu odtwarzania zachowaj stabilną wartość referencyjną (np. endTimeMillis lub startedAtMillis) zamiast aktualizować klucz PersistToDataStore co sekundę lub klatkę i obliczać czas, który upłynął, lub pozostały czas wyświetlania w interfejsie.
  • Przed zaktualizowaniem utrwalonego klucza odczekaj, aż użytkownik przestanie szybko wprowadzać dane: w przypadku wersji roboczych tekstu, zapytań lub suwaków przechowuj dane wejściowe w pamięci MutableState (lub StateStoreKey, który nie implementuje PersistToDataStore) i wywołuj setState na utrwalonym StateStoreKey dopiero po zakończeniu wprowadzania danych.
  • Klucze trwałe powinny być lekkie: wszystkie klucze PersistToDataStore w StateStore są serializowane razem w jednym pliku przy każdej aktualizacji. Utrzymuj kompaktowy stan sesji, taki jak aktywne identyfikatory, wybrane filtry czy wersje robocze formularzy w trakcie wypełniania, a duże kolekcje lub możliwe do przeszukiwania zbiory danych przechowuj w Room.