Модуль сохраненного состояния для ViewModel . Часть Android Jetpack .
Как упоминалось в разделе «Сохранение состояний пользовательского интерфейса» , объекты ViewModel
могут обрабатывать изменения конфигурации, поэтому вам не нужно беспокоиться о состоянии при ротации или других случаях. Однако если вам нужно справиться с завершением процесса, инициированным системой, вы можете использовать API SavedStateHandle
в качестве резервной копии.
Состояние пользовательского интерфейса обычно хранится или на него ссылаются в объектах ViewModel
, а не в действиях, поэтому для использования onSaveInstanceState()
или rememberSaveable
требуется некоторый шаблон, который модуль сохраненного состояния может обработать за вас.
При использовании этого модуля объекты ViewModel
получают объект SavedStateHandle
через его конструктор. Этот объект представляет собой карту «ключ-значение», которая позволяет записывать и извлекать объекты в сохраненное состояние и обратно. Эти значения сохраняются после завершения процесса системой и остаются доступными через тот же объект.
Сохраненное состояние привязано к вашему стеку задач. Если ваш стек задач исчезнет, ваше сохраненное состояние также исчезнет. Это может произойти при принудительной остановке приложения, удалении приложения из меню «Последние» или перезагрузке устройства. В таких случаях стек задач исчезает и восстановить информацию в сохраненном состоянии невозможно. В сценариях закрытия состояния пользовательского интерфейса, инициированных пользователем , сохраненное состояние не восстанавливается. В сценариях , инициируемых системой , это так.
Настраивать
Начиная с Fragment 1.2.0 или его транзитивной зависимости Activity 1.1.0 , вы можете принять SavedStateHandle
в качестве аргумента конструктора для вашей ViewModel
.
Котлин
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Ява
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
Затем вы можете получить экземпляр вашей ViewModel
без какой-либо дополнительной настройки. Фабрика ViewModel
по умолчанию предоставляет соответствующий SavedStateHandle
для вашей ViewModel
.
Котлин
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
Ява
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
, которые заключены в наблюдаемую LiveData
, с помощью getLiveData()
. Когда значение ключа обновляется, LiveData
получает новое значение. Чаще всего значение устанавливается в результате взаимодействия с пользователем, например ввода запроса для фильтрации списка данных. Это обновленное значение затем можно использовать для преобразования LiveData
.
Котлин
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 } }
Ява
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); } }
StateFlow
Получите значения из SavedStateHandle
, которые заключены в наблюдаемый StateFlow
, с помощью getStateFlow()
. Когда вы обновляете значение ключа, StateFlow
получает новое значение. Чаще всего вы можете установить значение в результате взаимодействия с пользователем, например ввода запроса для фильтрации списка данных. Затем вы можете преобразовать это обновленное значение с помощью других операторов Flow .
Котлин
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlow<List<String>> = savedStateHandle.getStateFlow<String>("query") .flatMapLatest { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Государственная поддержка Experimental Compose
Артефакт lifecycle-viewmodel-compose
предоставляет экспериментальные saveable
API, которые обеспечивают взаимодействие между SavedStateHandle
и Compose Saver
, так что любое 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+) |
Если класс не расширяет один из классов в приведенном выше списке, рассмотрите возможность разделения класса, добавив аннотацию @Parcelize
Kotlin или реализовав Parcelable
напрямую.
Сохранение непарцеллируемых классов
Если класс не реализует Parcelable
или Serializable
и не может быть изменен для реализации одного из этих интерфейсов, то невозможно напрямую сохранить экземпляр этого класса в SavedStateHandle
.
Начиная с жизненного цикла 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 } } }
Ява
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
:
Котлин
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 } } }
Ява
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
, когда пользователь вернется, извлеките Bundle
temp_file
из 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 } } }
Ява
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; } } }
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 отключен.
- Сохранение состояний пользовательского интерфейса
- Работа с наблюдаемыми объектами данных
- Создание моделей представления с зависимостями