В этом руководстве рассматриваются ожидания пользователей относительно состояния интерфейса и доступные варианты сохранения состояния.
Быстрое сохранение и восстановление состояния интерфейса после того, как система уничтожает основное действие или процесс приложения, необходимо для удобства пользователей. Пользователи ожидают, что состояние интерфейса останется прежним, но система может уничтожить активность, в которой размещен экран, и его сохраненное состояние.
Чтобы привести поведение системы в соответствие с ожиданиями пользователей, используйте следующие методы:
ViewModelобъектов.- Сохраненное состояние в следующих контекстах:
- Функции:
rememberSerializableиrememberSaveable. - ViewModel:
SavedStateHandle.
- Функции:
- Локальное хранилище для сохранения состояния интерфейса при переходе между приложениями и экранами.
Оптимальное решение зависит от сложности данных интерфейса, вариантов использования приложения и баланса между скоростью доступа к данным и использованием памяти.
Убедитесь, что приложение соответствует ожиданиям пользователей и работает быстро. Избегайте задержек при загрузке данных в интерфейс, особенно после распространенных изменений конфигурации, таких как поворот экрана.
Ожидания пользователей и поведение системы
В зависимости от действий пользователя состояние интерфейса должно либо сбрасываться, либо сохраняться. В некоторых случаях система автоматически выполняет то, что ожидает пользователь. В других случаях система делает наоборот.
Закрытие интерфейса пользователем
Пользователь ожидает, что при переходе на экран его временное состояние интерфейса останется прежним, пока он не закроет экран. Пользователь может полностью закрыть экран или приложение, выполнив следующие действия:
- смахнуть приложение с экрана обзора (недавних приложений);
- завершить работу приложения принудительно на экране настроек;
- Перезагрузка устройства.
- Выполнение какого-либо завершающего действия (которое поддерживается
Activity.finish()).
В таких случаях пользователь предполагает, что он навсегда покинул экран, и если он вернется, то ожидает, что экран будет в исходном состоянии. В этих случаях система ведет себя так, как ожидают пользователи: экземпляр основного действия уничтожается и удаляется из памяти вместе со всеми сохраненными в нем данными и связанными с ним записями сохраненного состояния.
Однако есть исключения из этого правила. Например, пользователь может ожидать, что браузер вернет его на ту же страницу, которую он смотрел до того, как закрыл браузер, нажав кнопку "Назад".
Закрытие интерфейса системой
Пользователь ожидает, что состояние интерфейса экрана останется прежним при изменении конфигурации, например при повороте экрана или переходе в многооконный режим. Однако по умолчанию система уничтожает основное действие при изменении конфигурации, удаляя все сохраненное в нем состояние интерфейса. Подробнее о конфигурациях устройств можно узнать в статье Как реагировать на изменения конфигурации в Jetpack Compose.
Обратите внимание, что можно переопределить поведение по умолчанию для изменений конфигурации (хотя это и не рекомендуется). Подробнее о том, как обрабатывать изменения конфигурации…
Пользователь также ожидает, что состояние интерфейса вашего приложения останется прежним, если он временно переключится на другое приложение, а затем вернется к вашему. Например, пользователь выполняет поиск на экране, а затем нажимает кнопку главного экрана или отвечает на телефонный звонок. Когда он возвращается к экрану поиска, он ожидает увидеть ключевое слово и результаты поиска в том же виде, в каком они были до этого.
В этом случае приложение переходит в фоновый режим, и система старается сохранить его процесс в памяти. Однако система может уничтожить процесс приложения, пока пользователь взаимодействует с другими приложениями. В таком случае основное действие уничтожается вместе со всеми сохраненными в нем данными. Когда пользователь перезапускает приложение, экран неожиданно оказывается в исходном состоянии. Подробнее о процессах и жизненном цикле приложений…
Варианты сохранения состояния интерфейса
Если ожидания пользователя относительно состояния интерфейса не совпадают с поведением системы по умолчанию, вы должны сохранить и восстановить состояние интерфейса, чтобы пользователь не заметил, что система уничтожила его.
Каждый из вариантов сохранения состояния интерфейса различается по следующим параметрам, влияющим на удобство использования:
| ViewModel | Сохраненное состояние | Постоянное хранилище | |
|---|---|---|---|
| Расположение хранилища | Хранение в памяти | Хранение в памяти | на диске или в сети; |
| Сохраняется при изменении конфигурации | Да | Да | Да |
| Сохраняет данные при завершении процесса системой | Нет | Да | Да |
Сохраняется после закрытия экрана пользователем/finish() |
Нет | Нет | Да |
| Ограничения данных | сложные объекты допустимы, но пространство ограничено доступной памятью; | только для примитивных типов и простых небольших объектов, таких как String. |
ограничивается только местом на диске или стоимостью / временем извлечения из сетевого ресурса. |
| Время чтения/записи | быстрый (только доступ к памяти); | медленный (требуется сериализация/десериализация); | медленно (требуется доступ к диску или сетевая транзакция); |
Используйте ViewModel для обработки изменений конфигурации
ViewModel идеально подходит для хранения и управления данными, связанными с интерфейсом, пока пользователь активно использует приложение. Он обеспечивает быстрый доступ к данным интерфейса и позволяет избежать повторного получения данных из сети или с диска при повороте экрана, изменении размера окна и других распространенных изменениях конфигурации. Информацию о том, как реализовать ViewModel, можно найти в руководстве по ViewModel.
ViewModel хранит данные в памяти, поэтому их извлечение обходится дешевле, чем извлечение данных с диска или из сети. ViewModel связана с владельцем жизненного цикла, например с пунктом назначения навигации или действием. Она остается в памяти при изменении конфигурации, и система автоматически связывает ViewModel с новым экземпляром владельца жизненного цикла, который появляется в результате изменения конфигурации.
В отличие от сохраненного состояния, объекты ViewModel уничтожаются, когда система завершает процесс. Чтобы перезагрузить данные после завершения процесса системой в ViewModel, используйте SavedStateHandle API. Если данные относятся к интерфейсу и их не нужно хранить в ViewModel, используйте rememberSerializable. Для примитивных типов данных или в случаях, когда вы не хотите использовать @Serializable, используйте rememberSaveable. Если это данные приложения, лучше сохранить их на диск.
Если вы уже используете решение для хранения состояния интерфейса в памяти при изменении конфигурации, вам может не понадобиться ViewModel.
Использовать сохраненное состояние в качестве резервной копии для обработки завершения процесса, инициированного системой
API, такие как rememberSerializable и rememberSaveable в Compose и SavedStateHandle в ViewModels, хранят данные, необходимые для перезагрузки состояния интерфейса, если система уничтожает, а затем воссоздает компонент. Чтобы эффективнее работать со сложными структурами данных, SavedStateHandle поддерживает Kotlinx Serialization через расширение saved {}, позволяя без проблем сохранять и восстанавливать объекты с безопасностью типов вместе со стандартными примитивными типами. Чтобы узнать, как реализовать сохраненное состояние с помощью rememberSaveable, ознакомьтесь со статьей Состояние и Jetpack Compose.
Сохраненные пакеты состояния сохраняются при изменении конфигурации и завершении процесса, но ограничены объемом хранилища и скоростью, поскольку разные API сериализуют данные. Сериализация может потреблять много памяти, если сериализуемые объекты сложные. Поскольку этот процесс выполняется в основном потоке во время изменения конфигурации, длительная сериализация может привести к пропуску кадров и визуальным задержкам.
Не используйте сохраненное состояние для хранения больших объемов данных, таких как растровые изображения, а также сложных структур данных, требующих длительной сериализации или десериализации. Вместо этого храните только примитивные типы и простые небольшие объекты, например String. Поэтому в сохраненном состоянии нужно хранить минимальный объем данных, например идентификатор, чтобы при сбое других механизмов сохранения можно было воссоздать данные, необходимые для восстановления интерфейса. Большинство приложений должны реализовывать этот метод, чтобы обрабатывать завершение процесса, инициированное системой.
В зависимости от того, как используется ваше приложение, вам может не понадобиться сохранять состояние. Например, браузер может вернуть пользователя на ту же страницу, которую он просматривал до выхода из браузера. Если ваше действие ведет себя таким образом, вы можете отказаться от использования сохраненного состояния и вместо этого сохранять все локально.
Кроме того, когда вы открываете объект activity из намерения, пакет дополнительных данных передается в объект activity как при изменении конфигурации, так и при восстановлении объекта activity системой. Если при запуске действия в качестве дополнительного параметра намерения передавались данные о состоянии интерфейса, например поисковый запрос, вместо пакета сохраненного состояния можно использовать пакет дополнительных параметров. Подробнее о дополнительных параметрах намерений…
В любом из этих случаев вам следует использовать ViewModel, чтобы избежать потери времени на перезагрузку данных из базы данных при изменении конфигурации.
Если данные интерфейса, которые нужно сохранить, простые и небольшие, для сохранения состояния можно использовать только API сохраненного состояния.
Как использовать сохраненное состояние с помощью SavedStateRegistry
Начиная с Fragment 1.1.0 или его транзитивной зависимости Activity 1.0.0, компоненты интерфейса, например ComponentActivity, реализуют SavedStateRegistryOwner и предоставляют SavedStateRegistry, связанный с этим компонентом. SavedStateRegistry позволяет компонентам подключаться к сохраненному состоянию, чтобы использовать его или дополнять. Например, модуль сохраненного состояния для ViewModel использует SavedStateRegistry, чтобы создать SavedStateHandle и предоставить его объектам ViewModel. Вы можете получить SavedStateRegistry из владельца жизненного цикла, вызвав метод savedStateRegistry.
Компоненты, которые сохраняют состояние, должны реализовывать интерфейс SavedStateRegistry.SavedStateProvider, определяющий один метод saveState(). Метод saveState() позволяет компоненту возвращать Bundle, содержащий любое состояние, которое должно быть сохранено из этого компонента.
SavedStateRegistry вызывает этот метод на этапе сохранения состояния жизненного цикла владельца.
class SearchManager : SavedStateRegistry.SavedStateProvider {
companion object {
private const val QUERY = "query"
}
private val query: String? = null
...
override fun saveState(): Bundle {
return bundleOf(QUERY to query)
}
}
Чтобы зарегистрировать SavedStateProvider, вызовите метод registerSavedStateProvider() на устройстве SavedStateRegistry, передав ключ для связывания с данными поставщика, а также самого поставщика. Ранее сохраненные данные поставщика можно получить из сохраненного состояния, вызвав метод consumeRestoredStateForKey() для объекта SavedStateRegistry и передав ключ, связанный с данными поставщика.
Внутри ComponentActivity можно зарегистрировать SavedStateProvider в onCreate() после вызова super.onCreate(). Вы также можете задать LifecycleObserver для SavedStateRegistryOwner, который реализует LifecycleOwner, и зарегистрировать SavedStateProvider после того, как произойдет событие ON_CREATE. Используя LifecycleObserver, вы можете отделить регистрацию и получение ранее сохраненного состояния от самого SavedStateRegistryOwner.
class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
companion object {
private const val PROVIDER = "search_manager"
private const val QUERY = "query"
}
private val query: String? = null
init {
// Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
// Register this object for future calls to saveState()
registry.registerSavedStateProvider(PROVIDER, this)
// Get the previously saved state and restore it
val state = registry.consumeRestoredStateForKey(PROVIDER)
// Apply the previously saved state
query = state?.getString(QUERY)
}
}
}
override fun saveState(): Bundle {
return bundleOf(QUERY to query)
}
...
}
class SearchActivity : ComponentActivity() {
private var searchManager = SearchManager(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set up your Compose UI here
setContent {
// ...
}
}
}
Используйте локальное хранилище для обработки завершения процесса при работе со сложными или большими объемами данных
Постоянное локальное хранилище, например база данных или DataStore, будет существовать, пока приложение установлено на устройстве пользователя (если только он не удалит данные приложения). Такое локальное хранилище сохраняется, даже если система завершает процесс приложения, но извлечение данных из него может быть дорогостоящим, поскольку их нужно будет считывать из локального хранилища в память. Часто постоянное локальное хранилище уже является частью архитектуры приложения, чтобы сохранять все данные, которые не должны быть потеряны при открытии и закрытии приложения.
ViewModel и состояние, сохраненное с помощью rememberSerializable, rememberSaveable или SavedStateHandle, не предназначены для долгосрочного хранения данных и не могут заменить локальное хранилище, например базу данных. Вместо этого используйте их только для временного хранения состояния интерфейса, а для других данных приложения – постоянное хранилище. Подробнее о том, как использовать локальное хранилище для долгосрочного хранения данных модели приложения (например, при перезапуске устройства), можно узнать из руководства по архитектуре приложений.
Управление состоянием интерфейса: разделяй и властвуй
Вы можете эффективно сохранять и восстанавливать состояние интерфейса, распределяя задачи между различными механизмами сохранения данных. В большинстве случаев каждый из этих механизмов должен хранить разные типы данных, используемых в приложении, с учетом компромиссов между сложностью данных, скоростью доступа и временем жизни:
- Локальное хранилище. В нем хранятся все данные приложения, которые не должны быть потеряны при открытии и закрытии приложения.
- Пример: коллекция объектов песен, которая может включать аудиофайлы и метаданные.
ViewModel: хранит в памяти все данные, необходимые для отображения связанного интерфейса, состояние интерфейса экрана.- Пример: объекты песен из последнего поиска и последний поисковый запрос.
- Сохраненное состояние (
rememberSerializable,rememberSaveableиSavedStateHandle). Хранит небольшой объем данных, необходимых для перезагрузки состояния интерфейса, если система останавливается, а затем воссоздает интерфейс. Вместо того чтобы хранить здесь сложные объекты, сохраняйте их в локальном хранилище и сохраняйте уникальный идентификатор этих объектов в API сохраненного состояния.- Пример: хранение последнего поискового запроса.
Например, в приложении можно искать песни в своей библиотеке. Вот как следует поступать в разных ситуациях:
Когда пользователь добавляет песню, ViewModel сразу же делегирует сохранение этих данных локально. Если вы хотите, чтобы добавленная композиция показывалась в интерфейсе, обновите данные в объекте ViewModel. Не забывайте выполнять все операции вставки в базу данных вне основного потока.
Когда пользователь ищет песню, все сложные данные о ней, которые вы загружаете из базы данных, должны быть немедленно сохранены в объекте ViewModel как часть состояния интерфейса экрана.
Когда приложение переходит в фоновый режим и система сохраняет его состояние, поисковый запрос должен сохраняться с помощью API сохраненного состояния на случай, если процесс будет воссоздан. Поскольку эта информация необходима для загрузки данных приложения, хранящихся в нем, сохраните поисковый запрос в ViewModel SavedStateHandle или используйте rememberSerializable или rememberSaveable в своих композициях. Это вся информация, которая вам понадобится, чтобы загрузить данные и восстановить текущее состояние интерфейса.
Восстановление сложных состояний: сборка фрагментов
Когда пользователь возвращается в приложение, возможны два сценария восстановления интерфейса:
- Интерфейс воссоздается после того, как система завершает процесс приложения. Система сохранила запрос с помощью API сохранения состояния. Функция
ViewModel(с использованиемSavedStateHandle) или composable-функция (с использованиемrememberSerializableилиrememberSaveable) автоматически восстанавливает запрос. Если composable-функция восстанавливает запрос, она передает его вViewModel.ViewModelвидит, что в кеше нет результатов поиска, и делегирует загрузку результатов поиска по заданному поисковому запросу. - После изменения конфигурации интерфейс создается заново. Поскольку экземпляр
ViewModelне был уничтожен, в его памяти хранится вся информация, и ему не нужно повторно запрашивать базу данных.ViewModel
Дополнительные ресурсы
Чтобы узнать больше о сохранении состояния интерфейса, ознакомьтесь со следующими ресурсами:
- Документация по слою интерфейса
- Сохранение состояния интерфейса в Compose
- Документация по состоянию и Jetpack Compose
Практические работы
- Состояние в Jetpack Compose
- Расширенные возможности управления состоянием и побочные эффекты в Jetpack Compose
Просмотр контента
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Модуль Saved State для ViewModel
- Как управлять жизненным циклом с помощью компонентов с поддержкой жизненного цикла
- Обзор ViewModel