Концепции и реализация Jetpack Compose
В этом руководстве рассматриваются ожидания пользователей относительно состояния пользовательского интерфейса и доступные варианты сохранения этого состояния.
Быстрое сохранение и восстановление состояния пользовательского интерфейса активности после того, как система завершит работу активности или приложения, имеет важное значение для обеспечения хорошего пользовательского опыта. Пользователи ожидают, что состояние пользовательского интерфейса останется неизменным, но система может уничтожить активность и её сохранённое состояние.
Для преодоления разрыва между ожиданиями пользователей и поведением системы используйте комбинацию следующих методов:
- Объекты
ViewModel. - Сохраненные состояния экземпляров в следующих контекстах:
- Представления: API
onSaveInstanceState(). - ViewModels:
SavedStateHandle.
- Представления: API
- Локальное хранилище для сохранения состояния пользовательского интерфейса во время переходов между приложениями и активностями.
Оптимальное решение зависит от сложности данных пользовательского интерфейса, сценариев использования вашего приложения и поиска баланса между скоростью доступа к данным и использованием памяти.
Убедитесь, что ваше приложение соответствует ожиданиям пользователей и предлагает быстрый и отзывчивый интерфейс. Избегайте задержек при загрузке данных в пользовательский интерфейс, особенно после распространенных изменений конфигурации, таких как поворот экрана.
Ожидания пользователей и поведение системы
В зависимости от действий пользователя, он ожидает, что состояние активности будет либо очищено, либо сохранено. В некоторых случаях система автоматически выполняет то, что ожидает пользователь. В других случаях система делает обратное.
Закрытие состояния пользовательского интерфейса по инициативе пользователя
Пользователь ожидает, что после запуска активности её временное состояние останется неизменным до тех пор, пока пользователь полностью не закроет её. Пользователь может полностью закрыть активность следующим образом:
- Удаление активности с экрана «Обзор» (Недавние).
- Завершение работы или принудительное закрытие приложения через экран настроек.
- Перезагрузка устройства.
- Завершение какого-либо действия (которое обеспечивается методом
Activity.finish()).
В случаях полного закрытия приложения пользователь предполагает, что он навсегда покинул приложение, и если он откроет его снова, то ожидает, что приложение запустится с чистого состояния. Поведение системы в таких сценариях закрытия соответствует ожиданиям пользователя: экземпляр приложения будет уничтожен и удален из памяти вместе со всем хранящимся в нем состоянием и всеми сохраненными записями состояния экземпляра, связанными с приложением.
Из этого правила о полном закрытии браузера есть некоторые исключения — например, пользователь может ожидать, что браузер перенаправит его на ту же самую веб-страницу, которую он просматривал до закрытия браузера с помощью кнопки «Назад».
Закрытие состояния пользовательского интерфейса, инициированное системой.
Пользователь ожидает, что состояние пользовательского интерфейса активности останется неизменным при изменении конфигурации, например, при повороте экрана или переключении в многооконный режим. Однако по умолчанию система уничтожает активность при таком изменении конфигурации, удаляя все данные о состоянии пользовательского интерфейса, хранящиеся в экземпляре активности. Для получения дополнительной информации о конфигурациях устройств см. страницу справочника по конфигурациям .
Обратите внимание, что можно (хотя и не рекомендуется) изменить поведение по умолчанию при изменении конфигурации. Дополнительные сведения см. в разделе «Обработка изменений конфигурации» .
Пользователь также ожидает, что состояние пользовательского интерфейса вашей активности останется неизменным, если он временно переключится на другое приложение, а затем вернется к вашему приложению. Например, пользователь выполняет поиск в вашей активности поиска, а затем нажимает кнопку «Домой» или отвечает на телефонный звонок — когда он вернется к активности поиска, он ожидает найти ключевое слово и результаты поиска точно так же, как и раньше.
В этом сценарии ваше приложение переводится в фоновый режим, и система делает все возможное, чтобы сохранить процесс приложения в памяти. Однако система может завершить процесс приложения, пока пользователь отвлекается и взаимодействует с другими приложениями. В таком случае экземпляр активности уничтожается вместе со всем хранящимся в нем состоянием. Когда пользователь перезапускает приложение, активность неожиданно оказывается в чистом состоянии. Чтобы узнать больше о завершении процесса, см. раздел «Процессы и жизненный цикл приложения» .
Параметры сохранения состояния пользовательского интерфейса
Если ожидания пользователя относительно состояния пользовательского интерфейса не соответствуют стандартному поведению системы, необходимо сохранить и восстановить состояние пользовательского интерфейса, чтобы гарантировать, что инициированное системой уничтожение данных будет незаметным для пользователя.
Каждый из вариантов сохранения состояния пользовательского интерфейса различается по следующим параметрам, влияющим на пользовательский опыт:
ViewModel | Сохраненное состояние экземпляра | Постоянное хранение | |
Место хранения | в память | в память | на диске или в сети |
Сохраняет изменения конфигурации | Да | Да | Да |
Выживает после завершения процесса, инициированного системой. | Нет | Да | Да |
Сохраняется после завершения действия пользователя (отмена/завершение()). | Нет | Нет | Да |
Ограничения данных | Сложные объекты — это хорошо, но пространство ограничено доступной памятью. | только для примитивных типов и простых, небольших объектов, таких как | Ограничения связаны только с объемом дискового пространства или стоимостью/временем извлечения данных из сетевого ресурса. |
Время чтения/записи | быстрый (только доступ к памяти) | Медленно (требует сериализации/десериализации) | Медленно (требует доступа к диску или сетевых транзакций) |
Используйте ViewModel для обработки изменений конфигурации.
ViewModel идеально подходит для хранения и управления данными, связанными с пользовательским интерфейсом, во время активного использования приложения пользователем. Он обеспечивает быстрый доступ к данным пользовательского интерфейса и помогает избежать повторной загрузки данных из сети или с диска при повороте экрана, изменении размера окна и других распространенных изменениях конфигурации. Чтобы узнать, как реализовать ViewModel, см. руководство по ViewModel .
ViewModel хранит данные в памяти, что означает, что их извлечение обходится дешевле, чем извлечение данных с диска или из сети. ViewModel связан с действием (или другим владельцем жизненного цикла) — он остается в памяти во время изменения конфигурации, и система автоматически связывает ViewModel с новым экземпляром действия, который возникает в результате изменения конфигурации.
ViewModels автоматически уничтожаются системой, когда пользователь выходит из активности или фрагмента, или если вы вызываете finish() , что означает очистку состояния, как и ожидает пользователь в этих сценариях.
В отличие от сохраненного состояния экземпляра, ViewModel уничтожается при завершении процесса по инициированной системой ошибке. Для перезагрузки данных после завершения процесса по инициированной системой ошибки в ViewModel используйте API SavedStateHandle . В качестве альтернативы, если данные относятся к пользовательскому интерфейсу и не требуют хранения в ViewModel, используйте onSaveInstanceState() . Если же данные являются данными приложения , то лучше сохранить их на диск.
Если у вас уже есть решение для хранения состояния пользовательского интерфейса в оперативной памяти при изменении конфигурации, вам, возможно, не понадобится использовать ViewModel.
Используйте сохраненное состояние экземпляра в качестве резервной копии для обработки завершения процесса, инициированного системой.
Коллбэк onSaveInstanceState() в системе View и SavedStateHandle в ViewModel хранят данные, необходимые для перезагрузки состояния контроллера пользовательского интерфейса, такого как активность или фрагмент, если система уничтожает и впоследствии создает этот контроллер заново. Чтобы узнать, как реализовать сохранение состояния экземпляра с помощью onSaveInstanceState , см. раздел «Сохранение и восстановление состояния активности» в руководстве по жизненному циклу активности .
Сохраненные пакеты состояния экземпляра сохраняются как при изменении конфигурации, так и при завершении процесса, но их возможности ограничены объемом памяти и скоростью, поскольку различные API сериализуют данные. Сериализация может потреблять много памяти, если сериализуемые объекты сложны. Поскольку этот процесс происходит в основном потоке во время изменения конфигурации, длительная сериализация может привести к выпадению кадров и визуальным зависаниям.
Не используйте сохраненное состояние экземпляра для хранения больших объемов данных, таких как растровые изображения, или сложных структур данных, требующих длительной сериализации или десериализации. Вместо этого храните только примитивные типы и простые, небольшие объекты, такие как String . Таким образом, используйте сохраненное состояние экземпляра для хранения минимального необходимого объема данных, например, идентификатора, чтобы восстановить данные, необходимые для возврата пользовательского интерфейса в предыдущее состояние в случае отказа других механизмов сохранения. Большинство приложений должны реализовать это для обработки завершения процесса, инициированного системой.
В зависимости от сценариев использования вашего приложения, вам может вообще не понадобиться сохранять состояние экземпляра. Например, браузер может вернуть пользователя на ту же самую веб-страницу, которую он просматривал перед закрытием браузера. Если ваше приложение ведет себя таким образом, вы можете отказаться от использования сохраненного состояния экземпляра и вместо этого сохранять все данные локально.
Кроме того, при открытии активности из интента пакет дополнительных данных доставляется активности как при изменении конфигурации, так и при восстановлении активности системой.
В любом из этих сценариев вам все равно следует использовать ViewModel , чтобы избежать траты ресурсов на перезагрузку данных из базы данных при изменении конфигурации.
В случаях, когда сохраняемые данные пользовательского интерфейса просты и не требуют больших ресурсов, для сохранения данных состояния можно использовать только API сохранения состояния экземпляра.
Подключитесь к сохраненному состоянию с помощью SavedStateRegistry.
Начиная с Fragment 1.1.0 или его транзитивной зависимости Activity 1.0.0 , контроллеры пользовательского интерфейса, такие как Activity или Fragment , реализуют SavedStateRegistryOwner и предоставляют объект SavedStateRegistry , привязанный к этому контроллеру. SavedStateRegistry позволяет компонентам подключаться к сохраненному состоянию вашего контроллера пользовательского интерфейса для его использования или пополнения. Например, модуль SavedState для ViewModel использует SavedStateRegistry для создания объекта SavedStateHandle и предоставления его объектам ViewModel . Вы можете получить SavedStateRegistry из вашего контроллера пользовательского интерфейса, вызвав getSavedStateRegistry .
Компоненты, которые участвуют в сохранении состояния, должны реализовывать интерфейс SavedStateRegistry.SavedStateProvider , который определяет единственный метод с именем saveState . Метод saveState() позволяет вашему компоненту возвращать Bundle содержащий любое состояние, которое должно быть сохранено этим компонентом. SavedStateRegistry вызывает этот метод на этапе сохранения состояния в жизненном цикле контроллера пользовательского интерфейса.
class SearchManager implements SavedStateRegistry.SavedStateProvider {
private static String QUERY = "query";
private String query = null;
...
@NonNull
@Override
public Bundle saveState() {
Bundle bundle = new Bundle();
bundle.putString(QUERY, query);
return bundle;
}
}
Для регистрации SavedStateProvider вызовите registerSavedStateProvider() для SavedStateRegistry , передав ключ, который будет связан с данными поставщика, а также самого поставщика. Ранее сохраненные данные для поставщика можно получить из сохраненного состояния, вызвав consumeRestoredStateForKey() для SavedStateRegistry , передав ключ, связанный с данными поставщика.
Внутри Activity или Fragment вы можете зарегистрировать 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 SearchFragment : Fragment() {
private var searchManager = SearchManager(this)
...
}
Java
class SearchManager implements SavedStateRegistry.SavedStateProvider {
private static String PROVIDER = "search_manager";
private static String QUERY = "query";
private String query = null;
public SearchManager(SavedStateRegistryOwner registryOwner) {
registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
if (event == Lifecycle.Event.ON_CREATE) {
SavedStateRegistry registry = registryOwner.getSavedStateRegistry();
// Register this object for future calls to saveState()
registry.registerSavedStateProvider(PROVIDER, this);
// Get the previously saved state and restore it
Bundle state = registry.consumeRestoredStateForKey(PROVIDER);
// Apply the previously saved state
if (state != null) {
query = state.getString(QUERY);
}
}
});
}
@NonNull
@Override
public Bundle saveState() {
Bundle bundle = new Bundle();
bundle.putString(QUERY, query);
return bundle;
}
...
}
class SearchFragment extends Fragment {
private SearchManager searchManager = new SearchManager(this);
...
}
Используйте локальное сохранение данных для обработки завершения процессов при работе со сложными или большими объемами данных.
Постоянное локальное хранилище, такое как база данных или общие настройки, будет существовать до тех пор, пока ваше приложение установлено на устройстве пользователя (если только пользователь не очистит данные для вашего приложения). Хотя такое локальное хранилище сохраняется после инициированных системой действий и завершения процессов приложения, его извлечение может быть дорогостоящим, поскольку данные необходимо будет считывать из локального хранилища в память. Часто это постоянное локальное хранилище уже может быть частью архитектуры вашего приложения для хранения всех данных, которые вы не хотите потерять при открытии и закрытии приложения.
Ни ViewModel, ни сохраненное состояние экземпляра не являются решениями для долговременного хранения и, следовательно, не заменяют локальное хранилище, такое как база данных. Вместо этого следует использовать эти механизмы только для временного хранения переходного состояния пользовательского интерфейса, а для других данных приложения использовать постоянное хранилище. Более подробную информацию о том, как использовать локальное хранилище для долговременного сохранения данных модели приложения (например, после перезагрузки устройства), см. в Руководстве по архитектуре приложений.
Управление состоянием пользовательского интерфейса: разделяй и властвуй.
Эффективное сохранение и восстановление состояния пользовательского интерфейса можно осуществить, распределив работу между различными механизмами сохранения данных. В большинстве случаев каждый из этих механизмов должен хранить данные разного типа, используемые в активности, исходя из компромисса между сложностью данных, скоростью доступа и временем жизни:
- Локальное сохранение данных: хранит все данные приложения, которые вы не хотите потерять при открытии и закрытии активности.
- Пример: Коллекция объектов песен, которая может включать аудиофайлы и метаданные.
-
ViewModel: хранит в памяти все данные, необходимые для отображения соответствующего пользовательского интерфейса, а именно состояние экрана .- Пример: Объекты песен из последнего поиска и последнего поискового запроса.
- Сохраненное состояние экземпляра: хранит небольшой объем данных, необходимых для перезагрузки состояния пользовательского интерфейса, если система останавливается, а затем пересоздает интерфейс. Вместо хранения сложных объектов здесь, сохраняйте сложные объекты в локальном хранилище и храните уникальный идентификатор для этих объектов в API сохранения состояния экземпляра.
- Пример: Сохранение последнего поискового запроса.
В качестве примера рассмотрим функцию поиска по вашей библиотеке песен. Вот как следует обрабатывать различные события:
Когда пользователь добавляет песню, ViewModel немедленно делегирует сохранение этих данных локально. Если добавленная песня должна отображаться в пользовательском интерфейсе, необходимо также обновить данные в объекте ViewModel , чтобы отразить добавление песни. Не забывайте выполнять все операции вставки в базу данных вне основного потока.
Когда пользователь ищет песню, любые сложные данные о песне, загруженные из базы данных, должны быть немедленно сохранены в объекте ViewModel как часть состояния пользовательского интерфейса экрана.
Когда активность переходит в фоновый режим и система вызывает API сохраненного состояния экземпляра, поисковый запрос должен быть сохранен в сохраненном состоянии экземпляра на случай повторного создания процесса. Поскольку эта информация необходима для загрузки данных приложения, сохраненных в этом состоянии, сохраните поисковый запрос в ViewModel SavedStateHandle . Это вся информация, необходимая для загрузки данных и возвращения пользовательского интерфейса в текущее состояние.
Восстановление сложных состояний: сборка фрагментов.
Когда пользователю необходимо вернуться к активности, возможны два сценария её повторного создания:
- Активность воссоздается после остановки системой. Система сохраняет запрос в пакете сохраненного состояния экземпляра, и пользовательский интерфейс должен передать запрос в
ViewModel, еслиSavedStateHandleне используется.ViewModelвидит, что у него нет кэшированных результатов поиска, и делегирует загрузку результатов поиска, используя заданный поисковый запрос. - Действие создается заново после изменения конфигурации. Поскольку экземпляр
ViewModelне был уничтожен, вся информация вViewModelкэшируется в памяти, и ей не нужно повторно обращаться к базе данных.
Дополнительные ресурсы
Чтобы узнать больше о сохранении состояний пользовательского интерфейса, ознакомьтесь со следующими ресурсами.
Блоги
- ViewModels: простой пример
- ViewModels: Сохранение состояния ,
onSaveInstanceState, Восстановление состояния пользовательского интерфейса и индикаторы загрузки