מודול שמאפשר לשמור מצב של ViewModel חלק מ-Android Jetpack.
כפי שצוין בקטע שמירה של מצבי ממשק משתמש, אובייקטים מסוג ViewModel יכולים לטפל בשינויים בהגדרות, כך שאין צורך לדאוג לגבי המצב בזמן רוטציות או במקרים אחרים. עם זאת, אם אתם צריכים לטפל במוות של תהליך שהמערכת יזמה, כדאי להשתמש ב-API SavedStateHandle כגיבוי.
בדרך כלל, המצב של ממשק המשתמש מאוחסן או מופיע באובייקטים מסוג ViewModel ולא בפעילויות, ולכן כדי להשתמש ב-onSaveInstanceState() או ב-rememberSaveable צריך קוד לדוגמה שמודול המצב השמור יכול לטפל בו בשבילכם.
כשמשתמשים במודול הזה, אובייקטים מסוג ViewModel מקבלים אובייקט SavedStateHandle דרך ה-constructor שלו. האובייקט הזה הוא מפה של מפתח/ערך שמאפשרת לכתוב ולאחזר אובייקטים מהמצב השמור אליו וממנו. הערכים האלה נשארים גם אחרי שהמערכת מפסיקה את התהליך, והם זמינים דרך אותו אובייקט.
המצב השמור קשור למחסנית המשימות. אם מחסנית המשימות תיעלם, גם המצב השמור ייעלם. המצב הזה יכול לקרות אם מפסיקים אפליקציה בכוח, מסירים אותה מתפריט האפליקציות האחרונות או מפעילים מחדש את המכשיר. במקרים כאלה, סטאק המשימות נעלם ולא ניתן לשחזר את המידע במצב השמור. בתרחישים של סגירה של מצב ממשק המשתמש ביוזמת המשתמש, המצב השמור לא משוחזר. בתרחישים שנשלחו על ידי המערכת, כן.
הגדרה
החל מ-Fragment 1.2.0 או מהתלות הטרנזיטיבית שלו, Activity 1.1.0, אפשר לקבל SavedStateHandle כארגומנטים של קונסטרוקטור ל-ViewModel.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
לאחר מכן תוכלו לאחזר מכונה של ViewModel בלי הגדרות נוספות. מפעל ViewModel שמוגדר כברירת מחדל מספק את SavedStateHandle המתאים ל-ViewModel.
Kotlin
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
Java
class MainFragment extends Fragment { private SavedStateViewModel vm; public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { vm = new ViewModelProvider(this).get(SavedStateViewModel.class); ... } ... }
כשמציינים מכונה מותאמת אישית של ViewModelProvider.Factory, אפשר להפעיל את השימוש ב-SavedStateHandle על ידי הרחבה של AbstractSavedStateViewModelFactory.
עבודה עם SavedStateHandle
הכיתה SavedStateHandle היא מפה של מפתח/ערך שמאפשרת לכתוב ולשלוף נתונים מהמצב השמור אליו וממנו באמצעות השיטות set() ו-get().
כשמשתמשים ב-SavedStateHandle, ערך השאילתה נשמר גם אחרי שהתהליך מסתיים, וכך המשתמש רואה את אותה קבוצת נתונים מסוננים לפני יצירת מחדש ואחריה, בלי שהפעילות או החלקיק יצטרכו לשמור את הערך הזה באופן ידני, לשחזר אותו ולהעביר אותו חזרה ל-ViewModel.
ל-SavedStateHandle יש גם שיטות אחרות שאפשר לצפות להן כשעובדים עם מפה של מפתח/ערך:
contains(String key)– בודקת אם יש ערך למפתח הנתון.remove(String key)– מסיר את הערך של המפתח הנתון.keys()– הפונקציה מחזירה את כל המפתחות שמכיל ה-SavedStateHandle.
בנוסף, אפשר לאחזר ערכים מ-SavedStateHandle באמצעות בעל נתונים שגלוי לכולם. רשימת הסוגים הנתמכים:
LiveData
אחזור ערכים מ-SavedStateHandle שמקובצים ב-observable של LiveData באמצעות getLiveData().
כשהערך של המפתח מתעדכן, הערך החדש מקבל את LiveData. בדרך כלל הערך מוגדר כתוצאה מאינטראקציות של משתמשים, כמו הזנת שאילתה לסינון רשימת נתונים. לאחר מכן אפשר להשתמש בערך המעודכן כדי לשנות את LiveData.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: LiveData<List<String>> = savedStateHandle.getLiveData<String>("query").switchMap { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle savedStateHandle; public LiveData<List<String>> filteredData; public SavedStateViewModel(SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; LiveData<String> queryLiveData = savedStateHandle.getLiveData("query"); filteredData = Transformations.switchMap(queryLiveData, query -> { return repository.getFilteredData(query); }); } public void setQuery(String query) { savedStateHandle.set("query", query); } }
StateFlow
אחזור ערכים מ-SavedStateHandle שמקובצים ב-observable של StateFlow באמצעות getStateFlow().
כשמעדכנים את ערך המפתח, הערך החדש מקבל את StateFlow. בדרך כלל, אפשר להגדיר את הערך כתוצאה מאינטראקציות של משתמשים, כמו הזנת שאילתה לסינון רשימת נתונים. לאחר מכן תוכלו לשנות את הערך המעודכן באמצעות אופרטורים אחרים של Flow.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlow<List<String>> = savedStateHandle.getStateFlow<String>("query") .flatMapLatest { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
תמיכה בסטטוס של Compose הניסיוני
הארטיפקט lifecycle-viewmodel-compose מספק את ממשקי ה-API הניסיוניים saveable שמאפשרים יכולת פעולה הדדית בין SavedStateHandle לבין Saver של Compose, כך שכל State שאפשר לשמור באמצעות rememberSaveable עם Saver מותאם אישית, אפשר לשמור גם באמצעות SavedStateHandle.
Kotlin
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 מכיל את הלוגיקה ליצירת הקובץ הזמני.
Kotlin
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel() { } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } }
כדי לוודא שהקובץ הזמני לא ילך לאיבוד אם התהליך של הפעילות יופסק ויוחזר מאוחר יותר, TempFileViewModel יכול להשתמש ב-SavedStateHandle כדי לשמור את הנתונים שלו. כדי לאפשר ל-TempFileViewModel לשמור את הנתונים שלו, מטמיעים את SavedStateProvider ומגדירים אותו כספק ב-SavedStateHandle של ה-ViewModel:
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } } }
כדי לשחזר את נתוני File כשהמשתמש יחזור, צריך לאחזר את temp_file
Bundle מה-SavedStateHandle. זהו אותו Bundle שסופק על ידי saveTempFile() ומכיל את הנתיב המוחלט. לאחר מכן אפשר להשתמש בנתיב המוחלט כדי ליצור מופע חדש של File.
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { Bundle tempFileBundle = savedStateHandle.get("temp_file"); if (tempFileBundle != null) { tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle); } savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } @Nullable private static File restoreTempFile(Bundle bundle) { if (bundle.containsKey("path") { return File(bundle.getString("path")); } return null; } } }
SavedStateHandle בבדיקות
כדי לבדוק ViewModel שמשתמש ב-SavedStateHandle כיחס תלות, יוצרים מכונה חדשה של SavedStateHandle עם ערכי הבדיקה הנדרשים ומעבירים אותה למכונה של ViewModel שאותה בודקים.
Kotlin
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) עם יחסי תלות