מודול Saved State ל-ViewModel   בארגז הכלים Android Jetpack.

כמו שמוזכר במאמר שמירת מצבי ממשק משתמש, אובייקטים מסוג ViewModel יכולים לטפל בשינויים בהגדרות האישיות, כך שלא צריך לדאוג לגבי המצב בסיבובים או במקרים אחרים. עם זאת, אם אתם צריכים לטפל בהשבתת תהליך שהמערכת יזמה, יכול להיות שתרצו להשתמש ב-API של SavedStateHandle כגיבוי.

מצב ממשק המשתמש בדרך כלל מאוחסן או מקושר באובייקטים של ViewModel, כך שהשימוש ב-rememberSaveable בפיתוח נייטיב דורש קוד סטנדרטי שהמודול של המצב השמור יכול לטפל בו בשבילכם.

כשמשתמשים במודול הזה, אובייקטים מסוג ViewModel מקבלים אובייקט מסוג SavedStateHandle דרך ה-constructor שלו. האובייקט הזה הוא מפה של מפתח/ערך שמאפשרת לכתוב ולאחזר אובייקטים אל המצב השמור וממנו. הערכים האלה נשארים גם אחרי שהמערכת סוגרת את התהליך, ואפשר להמשיך לגשת אליהם דרך אותו אובייקט.

המצב השמור מקושר למחסנית המשימות. אם מחסנית המשימות נעלמת, גם המצב השמור נעלם. זה יכול לקרות כשמאלצים עצירה של אפליקציה, מסירים את האפליקציה מתפריט האפליקציות האחרונות או מפעילים מחדש את המכשיר. במקרים כאלה, מחסנית המשימות נעלמת ואי אפשר לשחזר את המידע במצב השמור. בתרחישים של סגירת מצב ממשק המשתמש ביוזמת המשתמש, המצב השמור לא משוחזר. בתרחישים שהמערכת יוזמת, כן.

הגדרה

כדי להשתמש ב-SavedStateHandle, צריך לאשר אותו כארגומנט של פונקציית ה-constructor של ה-ViewModel.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

לאחר מכן תוכלו לאחזר מופע של ViewModel בתוך הרכיבים הניתנים לשילוב בלי הגדרות נוספות. ה-factory שמוגדר כברירת מחדל ב-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, ערך השאילתה נשמר גם אם התהליך מושבת. כך המשתמש רואה את אותה קבוצה של נתונים מסוננים לפני ואחרי היצירה מחדש, בלי שה-Activity או ה-Fragment יצטרכו לשמור, לשחזר ולהעביר את הערך הזה באופן ידני בחזרה אל ה-ViewModel.

ל-SavedStateHandle יש גם שיטות אחרות שאפשר לצפות להן כשמקיימים אינטראקציה עם מפה של מפתח/ערך:

  • ‫contains(String key) – בודקת אם יש ערך למפתח הנתון.
  • ‫remove(String key) – הסרת הערך של המפתח הנתון.
  • ‫keys() – מחזירה את כל המפתחות שכלולים ב-SavedStateHandle.

בנוסף, אפשר לאחזר ערכים מ-SavedStateHandle באמצעות מחזיק נתונים גלויים. רשימת הסוגים הנתמכים כוללת את הסוגים הבאים:

StateFlow

אפשר לאחזר ערכים מ-SavedStateHandle שעטופים ב-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

במצב מורכב של ממשק המשתמש, אפשר להשתמש ב-property delegate של saved לצד KotlinX Serialization. הנציג הזה מאפשר לשמור מחלקות נתונים מותאמות אישית של @Serializable ישירות ב-SavedStateHandle. כך המצב של ViewModel נשמר גם אם התהליך מושבת, וממשק המשתמש של Compose יכול לשחזר את המצב שלו בצורה חלקה כשהוא נוצר מחדש.

כדי להשתמש בו, מוסיפים הערה למחלקת הנתונים באמצעות @Serializable ומשתמשים בנציג 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)
    }
}

תמיכה ב-Compose State

אם המצב שלכם מסתמך על ממשקי ה-API של Saver ב-Compose במקום על KotlinX Serialization, פריט המידע שנוצר בתהליך הפיתוח (Artifact) של lifecycle-viewmodel-compose מספק את הנציג saveable. כך אפשר לשלב בין SavedStateHandle לבין Saver של Compose, כך שכל 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 באופן ישיר.

שמירת מחלקות שלא ניתנות להעברה (non-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
        }
    }
}

כדי לוודא שהקובץ הזמני לא יאבד אם התהליך של הפעילות יופסק וישוחזר מאוחר יותר, אפשר להשתמש ב-SavedStateHandle ב-TempFileViewModel כדי לשמור את הנתונים. כדי לאפשר ל-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

צפייה בתוכן