Модуль Saved State для ViewModel Часть Android Jetpack.
Как упоминалось в разделе Сохранение состояния интерфейса, объекты ViewModel могут обрабатывать изменения конфигурации, поэтому вам не нужно беспокоиться о состоянии при повороте экрана или в других случаях. Однако если вам нужно обрабатывать завершение процесса, инициированное системой, вы можете использовать API SavedStateHandle в качестве резервного.
Состояние интерфейса обычно хранится в объектах ViewModel или на них ссылается, поэтому при использовании rememberSaveable в Compose требуется определенный шаблон кода, который может обработать модуль сохраненного состояния.
При использовании этого модуля объекты ViewModel получают объект SavedStateHandle через конструктор. Этот объект представляет собой карту "ключ-значение", которая позволяет записывать и извлекать объекты из сохраненного состояния. Эти значения сохраняются, даже если система завершает процесс, и остаются доступными через тот же объект.
Сохраненное состояние связано со стеком задач. Если стек задач исчезнет, сохраненное состояние также будет удалено. Это может произойти, если принудительно остановить приложение, удалить его из меню последних приложений или перезагрузить устройство. В таких случаях стек задач исчезает, и вы не можете восстановить информацию в сохраненном состоянии. В сценариях закрытия интерфейса пользователем сохраненное состояние не восстанавливается. В инициированных системой сценариях это так.
Настроить
Чтобы использовать SavedStateHandle, примите его в качестве аргумента конструктора для ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
После этого вы сможете получить экземпляр ViewModel в своих композициях без дополнительной настройки. Фабрика 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 и viewModelFactory DSL.
Работа с SavedStateHandle
Класс SavedStateHandle представляет собой карту пар "ключ-значение", которая позволяет записывать и извлекать данные из сохраненного состояния с помощью методов set() и get().
При использовании SavedStateHandle значение запроса сохраняется при завершении процесса, поэтому пользователь видит один и тот же набор отфильтрованных данных до и после воссоздания, и при этом не нужно вручную сохранять, восстанавливать и передавать это значение обратно в 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
Для сложного состояния интерфейса можно использовать делегат свойства 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) } }
Поддержка статуса создания
Если в вашем приложении для управления состоянием используются API Saver из Compose, а не KotlinX Serialization, артефакт 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 для вашего приложения.
Типы, поддерживаемые напрямую
По умолчанию для объекта SavedStateHandle можно вызывать методы set() и get() для тех же типов данных, что и для объекта 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+) |
Если класс не наследует ни один из классов в списке выше, рассмотрите возможность сделать класс сериализуемым, добавив аннотацию Kotlin @Parcelize или реализовав Parcelable напрямую.
Сохранение классов, не поддерживающих сериализацию
Если класс не реализует интерфейсы Parcelable или Serializable и его нельзя изменить так, чтобы он реализовывал один из этих интерфейсов, то экземпляр этого класса нельзя напрямую сохранить в SavedStateHandle.
Начиная с версии Lifecycle 2.3.0-alpha03, SavedStateHandle позволяет сохранять любой объект, используя собственную логику сохранения и восстановления объекта в виде Bundle с помощью метода setSavedStateProvider().
SavedStateRegistry.SavedStateProvider – это интерфейс, который определяет один метод saveState(), возвращающий объект Bundle, содержащий состояние, которое вы хотите сохранить. Когда SavedStateHandle готов сохранить свое состояние, он вызывает saveState(), чтобы получить Bundle из SavedStateProvider, и сохраняет Bundle для связанного ключа.
Предположим, что приложение запрашивает изображение у приложения камеры с помощью намерения 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 можно найти в следующих ресурсах:
Практические работы
Просмотр контента
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Сохранение состояния интерфейса
- Как работать с наблюдаемыми данными
- Как создавать ViewModels с зависимостями