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

מושגים והטמעה ב-Jetpack פיתוח נייטיב

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

מצב ממשק המשתמש בדרך כלל מאוחסן באובייקטים של ViewModel או מתייחס אליהם, ולא לפעילויות. לכן, השימוש ב-onSaveInstanceState() דורש קוד boilerplate מסוים, שמודול המצב השמור יכול לטפל בו בשבילכם.

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

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

הגדרה

החל מ-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);
    }
}

סוגים נתמכים

הנתונים שנשמרים ב-SavedStateHandle נשמרים ומשוחזרים כ-Bundle, יחד עם שאר savedInstanceState של הפעילות או של הקטע.

שמירת מחלקות שלא ניתן להעביר

אם מחלקה לא מיישמת את 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;
        }
    }
}