מודול Saved State ל-ViewModel בארגז הכלים Android Jetpack.
כמו שצוין במאמר בנושא שמירת מצבי ממשק המשתמש, אובייקטים של ViewModel יכולים לטפל בשינויים בהגדרות, כך שלא צריך לדאוג לגבי מצב ברוטציות או במקרים אחרים. עם זאת, אם אתם צריכים לטפל בסיום תהליך שהמערכת יזמה, כדאי להשתמש ב-API SavedStateHandle כגיבוי.
מצב ממשק המשתמש בדרך כלל מאוחסן באובייקטים של ViewModel או שיש הפניה אליהם, ולכן השימוש ב-rememberSaveable ב-Compose דורש קוד boilerplate שמודול המצב השמור יכול לטפל בו בשבילכם.
כשמשתמשים במודול הזה, ViewModel אובייקטים מקבלים אובייקט SavedStateHandle דרך הקונסטרוקטור שלו. האובייקט הזה הוא מיפוי של מפתח וערך שמאפשר לכם לכתוב אובייקטים ולשלוף אותם מהמצב השמור. הערכים האלה נשמרים גם אחרי שהמערכת מפסיקה את התהליך, והם ממשיכים להיות זמינים דרך אותו אובייקט.
המצב השמור מקושר לערימת המשימות. אם עורמים את המשימות, המצב השמור נעלם. זה יכול לקרות כשמבצעים עצירה מאולצת של אפליקציה, כשמסירים את האפליקציה מתפריט האפליקציות האחרונות או כשמפעילים מחדש את המכשיר. במקרים כאלה, מחסנית המשימות נעלמת ואי אפשר לשחזר את המידע במצב השמור. בתרחישים של סגירת ממשק המשתמש על ידי המשתמש, המצב שנשמר לא משוחזר. בתרחישים של פעולות שמתבצעות על ידי המערכת, זה קורה.
הגדרה
כדי להשתמש ב-SavedStateHandle, צריך להגדיר אותו כארגומנט של בנאי ל-ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
לאחר מכן תוכלו לאחזר מופע של ViewModel בתוך רכיבי ה-Composable שלכם ללא הגדרה נוספת. ברירת המחדל של ViewModel מספקת את SavedStateHandle המתאים לViewModel.
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
כשמספקים מופע מותאם אישית של ViewModelProvider.Factory, אפשר להפעיל את השימוש ב-SavedStateHandle באמצעות CreationExtras ו-DSL של viewModelFactory.
עבודה עם SavedStateHandle
המחלקות SavedStateHandle הן מיפוי של מפתח וערך שמאפשר לכתוב נתונים במצב השמור ולקבל מהם נתונים באמצעות השיטות set() ו-get().
באמצעות SavedStateHandle, ערך השאילתה נשמר גם אחרי שהתהליך מסתיים. כך המשתמש רואה את אותה קבוצת נתונים מסוננים לפני ואחרי השחזור, בלי שהפעילות או הפריט יצטרכו לשמור, לשחזר ולהעביר את הערך הזה בחזרה אל ViewModel באופן ידני.
SavedStateHandle יש גם שיטות אחרות שאפשר לצפות להן כשמבצעים אינטראקציה עם מפה של זוגות מפתח/ערך:
-
contains(String key)– בודקת אם יש ערך למפתח הנתון. -
remove(String key)– מסיר את הערך של המפתח הנתון. -
keys()– מחזירה את כל המפתחות שכלולים ב-SavedStateHandle.
בנוסף, אפשר לאחזר ערכים מ-SavedStateHandle באמצעות מאגר נתונים גלויים. רשימת הסוגים הנתמכים כוללת את הסוגים הבאים:
StateFlow
אפשר לאחזר ערכים מ-SavedStateHandle שעטופים ב-observable של StateFlow. בהתאם לצורך לשנות את הערך ישירות, אפשר לבחור בין זרם לקריאה בלבד לבין זרם שניתן לשינוי:
-
getStateFlow(): משתמשים בזה אם רוצים רק לקרוא את המצב. כשמעדכנים את הערך של המפתח במקום אחר ב-SavedStateHandle, ה-StateFlow מקבל את הערך החדש. האפשרות הזו מתאימה במיוחד כשרוצים לחשוף זרם לקריאה בלבד ולהפוך אותו באמצעות אופרטורים של Flow. -
getMutableStateFlow(): משתמשים באפשרות הזו אם נדרשת גישת קריאה וגישת כתיבה. עדכון של.valueשלMutableStateFlowשמוחזר מעדכן אוטומטית אתSavedStateHandleהבסיסי, כך שלא צריך להגדיר את המפתח באופן ידני.
ברוב המקרים, אתם מעדכנים את הערכים האלה בעקבות אינטראקציות של משתמשים, כמו הזנת שאילתה לסינון רשימת נתונים.
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { // Use getMutableStateFlow to read and write the query directly private val _query = savedStateHandle.getMutableStateFlow("query", "") val query: StateFlow= _query.asStateFlow() // Use getStateFlow if you only need a read-only stream to react to changes val filteredData: StateFlow<List > = query.flatMapLatest { repository.getFilteredData(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun setQuery(newQuery: String) { // Updating the MutableStateFlow automatically updates the SavedStateHandle _query.value = newQuery } }
תמיכה ב-KotlinX Serialization
במקרים של מצב מורכב של ממשק המשתמש, אפשר להשתמש בנציג המאפיין saved לצד KotlinX Serialization. הנציג הזה מאפשר לכם לשמור נתונים מותאמים אישית של @Serializable
מחלקות נתונים ישירות ב-SavedStateHandle. כך נשמר המצב של ViewModel גם אחרי השבתת תהליך, כדי שממשק המשתמש של Compose יוכל לשחזר את המצב שלו בצורה חלקה אחרי יצירה מחדש.
כדי להשתמש בה, מוסיפים הערה למחלקת הנתונים עם @Serializable ומשתמשים ב-delegate saved
ב-ViewModel:
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel // Ensure you have the savedstate-ktx dependency import androidx.savedstate.serialization.saved import kotlinx.serialization.Serializable @Serializable data class UserFilterState( val searchQuery: String, val minAge: Int, val includeInactive: Boolean ) class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { // The state is automatically serialized to a Bundle on process death, // and deserialized upon recreation. var filterState by savedStateHandle.saved { UserFilterState(searchQuery = "", minAge = 18, includeInactive = false) } fun updateQuery(newQuery: String) { // Mutating the property automatically updates the underlying SavedStateHandle filterState = filterState.copy(searchQuery = newQuery) } }
תמיכה במצב כתיבה
אם המצב שלכם מסתמך על ממשקי ה-API של Saver ב-Compose במקום על KotlinX Serialization, ארטיפקט lifecycle-viewmodel-compose מספק את הנציג saveable. כך אפשר להשתמש ב-Saver של Compose עם SavedStateHandle, כך שכל State שאפשר לשמור באמצעות rememberSaveable עם Saver מותאם אישית, אפשר לשמור גם עם SavedStateHandle.
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
סוגים נתמכים
הנתונים שנשמרים ב-SavedStateHandle נשמרים ומשוחזרים כ-Bundle, יחד עם שאר savedInstanceState של האפליקציה.
סוגים נתמכים ישירות
כברירת מחדל, אפשר להפעיל את הפונקציות set() ו-get() ב-SavedStateHandle עבור אותם סוגי נתונים כמו ב-Bundle, כמו שמוצג בהמשך:
| תמיכה לפי סוג או סיווג | תמיכה במערכים |
double |
double[] |
int |
int[] |
long |
long[] |
String |
String[] |
byte |
byte[] |
char |
char[] |
CharSequence |
CharSequence[] |
float |
float[] |
Parcelable |
Parcelable[] |
Serializable |
Serializable[] |
short |
short[] |
SparseArray |
|
Binder |
|
Bundle |
|
ArrayList |
|
Size (only in API 21+) |
|
SizeF (only in API 21+) |
אם המחלקה לא מרחיבה אחת מהמחלקה שברשימה שלמעלה, כדאי להפוך את המחלקה ל-parcelable על ידי הוספת ההערה @Parcelize Kotlin או הטמעה ישירה של Parcelable.
שמירת מחלקות שלא ניתן להעביר
אם מחלקה לא מיישמת את Parcelable או Serializable ואי אפשר לשנות אותה כדי ליישם אחד מהממשקים האלה, אי אפשר לשמור ישירות מופע של המחלקה הזו ב-SavedStateHandle.
החל מ-Lifecycle 2.3.0-alpha03, SavedStateHandle מאפשרת לשמור כל אובייקט על ידי מתן לוגיקה משלכם לשמירה ולשחזור של האובייקט כ-Bundle באמצעות השיטה setSavedStateProvider().
SavedStateRegistry.SavedStateProvider הוא ממשק שמגדיר שיטה אחת של saveState() שמחזירה Bundle שמכיל את המצב שרוצים לשמור. כשהרכיב SavedStateHandle מוכן לשמור את המצב שלו, הוא קורא ל-saveState() כדי לאחזר את Bundle מ-SavedStateProvider ושומר את Bundle עבור המפתח המשויך.
לדוגמה, אפליקציה שמבקשת תמונה מאפליקציית המצלמה באמצעות intent ACTION_IMAGE_CAPTURE, ומעבירה קובץ זמני שבו המצלמה צריכה לשמור את התמונה. הלוגיקה ליצירת הקובץ הזמני הזה מוסתרת בתוך TempFileViewModel.
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
כדי לוודא שהקובץ הזמני לא יאבד אם התהליך של הפעילות יופסק ואחר כך ישוחזר, TempFileViewModel יכול להשתמש ב-SavedStateHandle כדי לשמור את הנתונים שלו. כדי לאפשר לאפליקציה TempFileViewModel לשמור את הנתונים שלה, צריך להטמיע את התג SavedStateProvider ולהגדיר אותו כספק בSavedStateHandle של ViewModel:
private fun File.saveTempFile() = bundleOf("path", absolutePath) class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
כדי לשחזר את הנתונים של File כשהמשתמש חוזר, צריך לאחזר את temp_file
Bundle מתוך SavedStateHandle. זהו אותו Bundle שסופק על ידי saveTempFile() ומכיל את הנתיב המוחלט. אחר כך אפשר להשתמש בנתיב המוחלט כדי ליצור מופע חדש של File.
private fun File.saveTempFile() = bundleOf("path", absolutePath) private fun Bundle.restoreTempFile() = if (containsKey("path")) { File(getString("path")) } else { null } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { val tempFileBundle = savedStateHandle.get<Bundle>("temp_file") if (tempFileBundle != null) { tempFile = tempFileBundle.restoreTempFile() } savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
SavedStateHandle בבדיקות
כדי לבדוק ViewModel שמשתמש ב-SavedStateHandle כתלות, צריך ליצור מופע חדש של SavedStateHandle עם ערכי הבדיקה הנדרשים ולהעביר אותו למופע ViewModel שאתם בודקים.
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
מקורות מידע נוספים
מידע נוסף על מודול Saved State ל-ViewModel זמין במקורות המידע הבאים.
Codelabs
צפיות בתוכן
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- שמירת מצבי ממשק המשתמש
- עבודה עם אובייקטים של נתונים גלויים
- יצירת ViewModels עם תלויות