Модуль Saved State для ViewModel Входит в Android Jetpack.
Как упоминалось в разделе Сохранение состояний пользовательского интерфейса, объекты ViewModel могут обрабатывать изменения конфигурации, поэтому вам не нужно беспокоиться о состоянии при поворотах или в других случаях. Однако если вам нужно обрабатывать завершение процесса, инициированное системой, вы можете использовать API SavedStateHandle в качестве резервного.
Состояние пользовательского интерфейса обычно хранится в объектах ViewModel или на них есть ссылки. Поэтому для использования rememberSaveable в Compose требуется шаблонный код, который может обрабатывать модуль сохраненного состояния.
При использовании этого модуля объекты 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, заключенные в наблюдаемый объект 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
Для сложного состояния пользовательского интерфейса вы можете использовать делегат свойства saved вместе с сериализацией KotlinX. Этот делегат позволяет сохранять пользовательские классы данных @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 Compose Saver, а не от 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 вашего приложения.
Типы, поддерживаемые напрямую
По умолчанию вы можете вызывать 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+) |
Если класс не расширяет ни один из перечисленных выше, сделайте его сериализуемым, добавив аннотацию Kotlin @Parcelize или напрямую реализовав Parcelable.
Сохранение классов, не поддерживающих 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) } }
Дополнительные ресурсы
Дополнительную информацию о модуле сохраненного состояния для ViewModel можно найти в следующих ресурсах:
Практические работы
Просмотр контента
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Сохранение состояний пользовательского интерфейса
- Как работать с объектами наблюдаемых данных
- Создание ViewModel с зависимостями