מודול 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

צפיות בתוכן