StateStore   Android Jetpack の一部。

StateStore は、共有アプリケーションの状態を Compose State としてアプリ全体に保存して公開する、タイプセーフなコンテナです。

ユースケース

アプリケーション スコープの StateStore を作成することで、同じプロセス内のアプリケーション コンポーネント間で状態を共有できます。

  • 複数のアクティビティ: アプリが複数のアクティビティを同時に表示する場合(メール クライアントの受信トレイ アクティビティとエディタ アクティビティなど)、またはマルチインスタンスをサポートするアクティビティ(ドキュメント ビューア アクティビティなど)がある場合は、StateStore を使用してアクティビティ間で情報を共有できます。
  • アクティビティとサービス: アクティビティとバックグラウンドの CarAppService または AppFunction の間で状態を同期します。
  • Activity とコンテンツ プロバイダまたはウィジェット: Activity とホーム画面ウィジェットの間で状態を共有します。

シングルトンが、キー付きルックアップや DataStore を利用したプロセスの終了の復元を必要とせず、メモリ内 State<T> のみを必要とする場合は、代わりにシングルトンで 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 クラスや依存関係挿入モジュールなど)で 1 回インスタンス化し、状態の読み取りと書き込みを行うマネージャーまたはリポジトリに挿入する通常のクラスです。StateStore を 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,
)

読み取りと書き込みの状態

マネージャーまたはリポジトリ内で、非公開の StateStoreKey 定義を使用して、挿入された StateStore の値を読み取り、書き込み、更新し、読み取り専用の State<T> プロパティとミューテーション関数を呼び出し元に公開します。

既読ステータス

getState(stateKey, defaultValue) を使用して State<T> オブジェクトを取得します。キーがまだ初期化されていない場合、StateStore は defaultValue パラメータでキーを初期化します。このパラメータのデフォルトは stateKey.defaultValue です(または、未初期化のキーを初期化するときにカスタム defaultValue を渡すこともできます)。getState は State<T> を返すため、ViewModel、UI コンポーザブル、ウィジェット、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 を実装する: PersistToDataStore インターフェースを実装して、永続化する必要がある StateStoreKey をマークします。

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

  1. StateStore を DataStore にバインドする: 共有 StateStore インスタンスを設定するときに(Application クラスや依存関係挿入モジュールなどで)syncToDataStore を 1 回呼び出します。syncToDataStore は、ディスクから保存された値を復元し、指定された coroutineScope で今後の変更を保存し続けるために無期限に一時停止する suspend 関数であるため、launch 内で呼び出し、アプリケーション レベルの CoroutineScope を渡します。起動時に、getState は最初にメモリ内の defaultValue を返し、DataStore がディスクからの読み取りを完了すると State を更新します。

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 キーの値が変更されるたびにシリアル化してディスクに書き込むため、何をいつ永続化するかを選択する際は、次のガイドラインに沿ってください。

  • タイマーのティックごとに保存するのではなく、開始または終了のタイムスタンプを保存する: タイマーや再生の進行状況については、PersistToDataStore キーを 1 秒ごとまたはフレームごとに更新するのではなく、安定した参照値(endTimeMillis や startedAtMillis など)を保持し、UI でライブの経過時間または残り時間を計算します。
  • 永続化されたキーを更新する前に、ユーザーの急速な入力をデバウンスする: テキストの下書き、検索クエリ、スライダーの場合、即時入力をメモリ内の MutableState(または PersistToDataStore を実装しない StateStoreKey)に保持し、入力が確定した後にのみ、永続化された StateStoreKey で setState を呼び出します。
  • 永続化されたキーを軽量に保つ: StateStore 内のすべての PersistToDataStore キーは、更新のたびに 1 つのファイルにシリアル化されます。アクティブな ID、フィルタの選択、進行中のフォームの下書きなど、コンパクトなセッション状態を保持し、大規模なコレクションやクエリ可能なデータセットを Room に保持します。