State<T> ו-MutableState<T> ב-Jetpack Compose הם מאגרי ערכים למטרות כלליות, שמגובים על ידי מערכת תמונת המצב של Compose. למרות שהם משמשים בעיקר להרכבה מחדש של ממשק המשתמש, מערכת תמונת המצב עצמה לא קשורה לעיבוד של ממשק המשתמש.
בעזרת הספריות של AppState, Transform ו-StateStore, אתם יכולים לשמור, להפיק, לראות ולשמר את State של Compose ב-ViewModels, במנהלים, במאגרים ובשירותים שפועלים ברקע.
למה כדאי להשתמש ב-Compose State מעבר לממשק המשתמש?
כשקוראים את State של Compose בתוך בלוק של מעקב אחרי מצב – כמו רכיב UI קומפוזבילי, transform או derivedStateOf – זמן הריצה של Compose מספק שני יתרונות מרכזיים בכל שכבה של הארכיטקטורה:
- מעקב אוטומטי ודינמי אחרי יחסי תלות: קריאת
.valueשל מצב (או שימוש בהקצאת נכסby) רושמת באופן אוטומטי את המצב הזה כתלות של הבלוק. מכיוון שהמעקב הוא דינמי, יחסי התלות נקבעים בזמן הריצה לפי נתיבי הקוד שמופעלים. אם לא נכנסים להסתעפות מותנית (לדוגמה, אחרי בדיקתifאוreturnמוקדם), המצבים בתוך ההסתעפות הזו לא נקראים ושינויים בהם לא מפעילים חישובים מחדש מיותרים. - עקביות ורמת פירוט: אתם יכולים להצהיר על מאפייני
MutableStateנפרדים ומפורטים, ולעטוף כתיבה של כמה מאפיינים ב-Snapshot.withMutableSnapshot. עדכוני ביניים מבודדים לתמונת המצב הניתנת לשינוי, והם מיושמים באופן אטומי כשהבלוק מסתיים. לכן, משתמשים שקוראים בתוך תמונת מצב אף פעם לא רואים מצב ביניים לא עקבי, והם מחשבים מחדש רק כשהמאפיינים הספציפיים שהם קוראים משתנים. בניגוד ל-StateFlow, אתם לא צריכים לבחור ביןStateFlow<UiState>מונוליטי יחיד (שמבטל כל אוסף כששדה כלשהו משתנה) לבין מאפייניStateFlowנפרדים (שלא יכולים להתעדכן יחד בטרנזקציה אטומית אחת).
בחירת ה-API הנכון למצב בארכיטקטורה שלכם
כדי לבחור איך לשמור את State של Compose, צריך להחליט כמה זמן המצב צריך להימשך ולאילו רכיבים צריכה להיות גישה אליו:
| קטגוריית המדינה | משך החיים וההיקף | דוגמאות | ממשקי API מומלצים של Compose State |
|---|---|---|---|
| מצב פעיל | הנתונים תמיד מאוחסנים בזיכרון, נשלפים מחדש בזמן ההפעלה ולא נשמרים אחרי השבתת התהליך. | סטטוס החיבור לרשת, סטטוס החיבור ל-Bluetooth, מצב זמני של ממשק המשתמש כמו בחירת פריט. |
|
| מצב זמני של ממשק המשתמש | הנתונים נשמרים גם אחרי שינויים בהגדרות וגם אחרי שהמערכת משביתה את התהליך, אבל לא נשמרים ללא הגבלת זמן. הם מוגבלים למסך אחד או לפעילות אחת. | רכיבים מורחבים או מכווצים בממשק המשתמש, מיקום הגלילה, שאילתת החיפוש במסך או מתגי הסינון. |
|
| מצב סשן זמני | הנתונים שורדים השבתת תהליך ברקע, לא נשמרים ללא הגבלת זמן; משותפים בין פעילויות, שירותים או ווידג'טים. | טיימר פעיל של מצב ריכוז, עגלת קניות עם תהליך תשלום בכמה שלבים או טיוטת טופס בתהליך מילוי. |
|
| מצב קבוע | נשמרים בדיסק ללא הגבלת זמן. | העדפות והגדרות של המשתמש (DataStore), נתוני אפליקציה מובְנים וספריות מדיה (Room). | DataStore או תהליכי Room שנאספו בתוך transform באמצעות collectAsState |
שימוש ב-Compose State ב-ViewModel
בתוך ViewModel, אתם יכולים להצהיר על קלט מסך עצמאי כמאפייני MutableState מפורטים, לשמור אותם במקרה של השבתת תהליך באמצעות SavedStateHandle.saveable, לעדכן כמה מאפיינים באופן אטומי באמצעות Snapshot.withMutableSnapshot ולשלב אותם עם זרמי Flow של מאגר בתוך transform:
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
הדפוסים העיקריים בViewModel:
- שימוש ב-
SavedStateHandle.saveableלמצב שמור ריאקטיבי: העברתsearchQueryל-SavedStateHandle.saveableעםmutableStateOf(מ-androidx.lifecycle:lifecycle-viewmodel-compose) שומרת את הערך במקרה של השבתת תהליך בתורMutableState, כך ש-transformעוקב אחרי קריאות באופן אוטומטי. הימנעו משימוש ב-SavedStateHandle.savedבלבד, שמחזיר מאפיין Kotlin פשוט במקוםStateשל Compose, ולא מפעיל הרכבה מחדש כשהוא משתנה. - כתיבה אטומית של כמה מאפיינים באמצעות
Snapshot.withMutableSnapshot: עדכון שלsearchQueryו-selectedTaskIdsבתוךSnapshot.withMutableSnapshotמבודד את הכתיבה ומבצע את שני השינויים בטרנזקציה אחת של תמונת מצב אטומית, כך שהצופים (כוללtransform) רואים את שני העדכונים יחד במעבר אחד של הרכבה מחדש, במקום לראות אתsearchQueryהחדש בשילוב עםselectedTaskIdsהישן. - שימוש חוזר בתהליכים בין קומפוזיציות מחדש באמצעות
remember(key): כשעוטפים אתtasksRepository.getTasks(searchQuery)ב-rememberעםsearchQueryכמפתח, מבטיחים שכאשרselectedTaskIdsמשתנה ויוצר מחדש את הבלוקtransform, Compose משתמש מחדש ב-Flowהקיים של מסד הנתונים במקום להפעיל מחדש את השאילתה.
שיתוף מצב המערכת בזמן אמת באפליקציה
במצב מערכת בזמן אמת שמשותף בין כמה ViewModels, פעילויות או שירותים – כמו סטטוס החיבור לרשת או ל-Bluetooth – לא כדאי לשמור סטטוס לא עדכני בדיסק במקרה של השבתת תהליך. במקום זאת, צריך לשלוח שאילתה לגבי מצב המערכת הנוכחי בזמן ההפעלה, לשמור אותו במאפיין mutableStateOf פרטי בתוך סינגלטון ברמת האפליקציה, ולחשוף State<T> לקריאה בלבד:
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState תומך בקריאות ובעדכונים עקביים מכל שרשור, כך ש-NetworkCallback יכול לעדכן את _isOnline.value ישירות משרשור הקריאה החוזרת (callback) ברקע. אם קריאה חוזרת ברקע מעדכנת כמה מאפיינים של MutableState ביחד, צריך לעטוף את פעולות הכתיבה ב-Snapshot.withMutableSnapshot כדי שהן יחולו באופן אטומי.
כל רכיב באפליקציה יכול לקרוא את networkMonitor.isOnline.value – בין אם כבדיקה חד-פעמית, בתוך רכיב UI קומפוזבילי, או בתוך בלוק של מעקב אחרי ערך דינמי כמו listener, derivedStateOf או transform. לדוגמה, TaskListViewModel יכול לקרוא את networkMonitor.isOnline.value בתוך הבלוק transform שלו:
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
transform עוקב אחרי networkMonitor.isOnline.value לצד searchQuery, selectedTaskIds והמאגר Flow, ולכן כל שינוי בקישוריות מחשב מחדש באופן אוטומטי את TaskListState.Success בלי לשלוח שאילתה מחדש למסד הנתונים.