‫StateStore   בארגז הכלים Android Jetpack.

‫StateStore הוא קונטיינר עם מניעת שגיאות הקלדה שמאחסן וחושף את מצב האפליקציה המשותף כ-Compose State באפליקציה.

תרחישים לדוגמה

אם יוצרים 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>, רכיבי ViewModel, רכיבי UI composable, ווידג'טים ובלוקים של 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.