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>,
)
- הטמעה של
PersistToDataStore: מסמנים את כל ה-StateStoreKeyשרוצים לשמור באמצעות הטמעה של הממשקPersistToDataStore:
@Serializable private data object FocusSessionKey : StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle), PersistToDataStore
- קישור של
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.