מודול שמאפשר לשמור מצב של 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) עם יחסי תלות