StateStore   Часть Android Jetpack.

StateStore – это типобезопасный контейнер, который хранит и предоставляет общее состояние приложения в виде State Compose.

Примеры использования

Создав StateStore в области действия приложения, вы можете передавать состояние между компонентами приложения в рамках одного процесса:

  • Несколько действий. Если в приложении одновременно выполняется несколько действий (например, в почтовом клиенте открыты папка "Входящие" и редактор писем) или действие поддерживает многооконный режим (например, в средстве просмотра документов), вы можете использовать StateStore для обмена информацией между ними.
  • Действие и сервис. Синхронизация состояния между действием и фоновым сервисом CarAppService или AppFunction.
  • Activity и поставщик контента или виджет. Обмен данными между Activity и виджетом на главном экране.

Если синглтону требуется только State<T> в памяти без поиска по ключу или восстановления после завершения процесса с поддержкой DataStore, вы можете вместо этого предоставить свойства mutableStateOf непосредственно в синглтоне. Чтобы сравнить API состояний в приложении, прочитайте статью Как выбрать подходящий API состояний для архитектуры приложения.

Обзор API

Пакет androidx.appstate.statestore содержит классы StateStore и 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,
)

Укажите штат

Чтобы обеспечить типобезопасность при хранении и получении значений, StateStore использует класс StateStoreKey для определения ключей, используемых для чтения и записи значений. Определения StateStoreKey (private или internal) должны находиться в менеджере, репозитории или модуле, которому принадлежит этот фрагмент состояния.

@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

StateStore – это обычный класс, экземпляр которого создается один раз в области приложения или модуля (например, в классе Application или модуле внедрения зависимостей) и внедряется в менеджеры или репозитории, которые считывают и записывают состояние, а не предоставляют StateStore непосредственно классам пользовательского интерфейса:

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

Чтение и запись состояния

В менеджере или репозитории считывайте, записывайте и обновляйте значения во внедренном StateStore, используя закрытые определения StateStoreKey, и предоставляйте вызывающим объектам доступ к свойствам State<T>, предназначенным только для чтения, и функциям изменения.

Статус прочтения

Используйте getState(stateKey, defaultValue), чтобы получить объект State<T>. Если ключ ещё не инициализирован, StateStore заполняет его параметром defaultValue, который по умолчанию равен stateKey.defaultValue (или вы можете передать пользовательский defaultValue при инициализации незаполненного ключа). Поскольку getState возвращает State<T>, ViewModels, UI composables, виджеты и блоки transform могут считывать свойство менеджера реактивно:

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

Запись данных о состоянии

Используйте setState(stateKey, value) в менеджере, чтобы инициализировать или заменить значение, связанное с ключом:

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

Как обновить состояние

Если вам нужно обновить состояние на основе его текущего значения, используйте updateState. Это позволяет получить текущее значение (или использовать значение по умолчанию, если оно не задано) и применить лямбда-функцию обновления:

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

Интеграции

StateStore интегрируется с Jetpack DataStore, чтобы сохранять и восстанавливать состояние при завершении процесса и перезапуске приложения.

DataStore

Артефакт androidx.appstate.statestore:statestore-datastore позволяет один раз настроить сохранение в общем экземпляре StateStore, чтобы сохраненные ключи любого менеджера автоматически сохранялись и восстанавливались с диска с помощью DataStore.

Пакет androidx.appstate.datastore предоставляет следующие 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. Реализуйте интерфейс PersistToDataStore. Пометьте все объекты StateStoreKey, которые должны сохраняться, реализовав интерфейс PersistToDataStore.

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

  1. Привяжите StateStore к DataStore. Вызовите syncToDataStore один раз при настройке общего экземпляра StateStore (например, в классе Application или модуле внедрения зависимостей). Поскольку syncToDataStore – это функция suspend, которая приостанавливается на неопределенный срок, чтобы восстановить сохраненные значения с диска и продолжить сохранять будущие изменения в предоставленном coroutineScope, вызовите ее внутри launch и передайте CoroutineScope на уровне приложения. При запуске getState сначала возвращает defaultValue в памяти и обновляет State, как только DataStore завершает чтение с диска:

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

Рекомендации по сохранению данных в DataStore

Поскольку syncToDataStore сериализует и записывает данные на диск при каждом изменении значения ключа PersistToDataStore, следуйте этим рекомендациям при выборе того, что и когда сохранять:

  • Сохраняйте временные метки начала или окончания вместо каждого тика таймера. Для таймеров или прогресса воспроизведения сохраняйте стабильное эталонное значение (например, endTimeMillis или startedAtMillis), а не обновляйте ключ PersistToDataStore каждую секунду или кадр, и вычисляйте текущее прошедшее или оставшееся время в пользовательском интерфейсе.
  • Устраняйте дребезг контактов при быстром вводе данных пользователем, прежде чем обновлять сохраненный ключ. Для черновиков текста, поисковых запросов или ползунков сохраняйте немедленный ввод в памяти MutableState (или StateStoreKey, который не реализует PersistToDataStore) и вызывайте setState для сохраненного StateStoreKey только после того, как ввод стабилизируется.
  • Используйте простые ключи. Все ключи PersistToDataStore в StateStore сериализуются в один файл при каждом обновлении. Сохраняйте компактное состояние сеанса, например активные идентификаторы, выбранные фильтры или черновики форм, а большие коллекции или наборы данных, доступные для запросов, храните в Room.