מודול שמאפשר לשמור מצב של 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