Сохранение состояний пользовательского интерфейса

В этом руководстве рассматриваются ожидания пользователей относительно состояния пользовательского интерфейса и доступные варианты сохранения этого состояния.

Быстрое сохранение и восстановление состояния пользовательского интерфейса после завершения системой основного процесса или приложения имеет решающее значение для обеспечения удобного взаимодействия с пользователем. Пользователи ожидают, что состояние пользовательского интерфейса останется неизменным, но система может уничтожить основной процесс, на котором отображается экран, и его сохраненное состояние.

Для преодоления разрыва между ожиданиями пользователей и поведением системы используйте комбинацию следующих методов:

  • Объекты ViewModel .
  • Сохраненное состояние в следующих контекстах:
  • Локальное хранилище для сохранения состояния пользовательского интерфейса во время переходов между приложениями и экранами.

Оптимальное решение зависит от сложности данных пользовательского интерфейса, сценариев использования вашего приложения и поиска баланса между скоростью доступа к данным и использованием памяти.

Убедитесь, что ваше приложение соответствует ожиданиям пользователей и предлагает быстрый и отзывчивый интерфейс. Избегайте задержек при загрузке данных в пользовательский интерфейс, особенно после распространенных изменений конфигурации, таких как поворот экрана.

Ожидания пользователей и поведение системы

В зависимости от действий пользователя, он ожидает, что состояние пользовательского интерфейса будет либо очищено, либо сохранено. В некоторых случаях система автоматически выполняет то, что ожидает пользователь. В других случаях система поступает наоборот.

Закрытие состояния пользовательского интерфейса по инициативе пользователя

Пользователь ожидает, что при переходе на экран его временное состояние пользовательского интерфейса останется неизменным до тех пор, пока он полностью его не закроет. Пользователь может полностью закрыть экран или приложение, выполнив следующие действия:

  • Удалите приложение с экрана «Обзор» (Недавние).
  • Завершение работы или принудительное закрытие приложения через экран настроек.
  • Перезагрузка устройства.
  • Завершение какого-либо действия (которое обеспечивается методом Activity.finish() ).

В случаях полного закрытия окна пользователь предполагает, что он навсегда покинул экран, и если он вернется, то ожидает, что экран начнет работу с чистого состояния. Базовое поведение системы в этих сценариях закрытия соответствует ожиданиям пользователя: экземпляр активности будет уничтожен и удален из памяти вместе со всем хранящимся в нем состоянием и всеми связанными с ним сохраненными записями состояния.

Из этого правила о полном закрытии браузера есть некоторые исключения — например, пользователь может ожидать, что браузер перенаправит его на ту же самую веб-страницу, которую он просматривал до закрытия браузера с помощью кнопки «Назад».

Закрытие состояния пользовательского интерфейса, инициированное системой.

Пользователь ожидает, что состояние пользовательского интерфейса экрана останется неизменным при изменении конфигурации, например, при повороте экрана или переключении в многооконный режим. Однако по умолчанию система уничтожает активность хоста при таком изменении конфигурации, удаляя все сохраненное в ней состояние пользовательского интерфейса. Подробнее о конфигурациях устройств см. в разделе «Реакция на изменения конфигурации в Jetpack Compose» .

Обратите внимание, что можно (хотя и не рекомендуется) изменить поведение по умолчанию при изменении конфигурации. Дополнительные сведения см. в разделе «Обработка изменений конфигурации» .

Пользователь также ожидает, что состояние пользовательского интерфейса вашего приложения останется неизменным, если он временно переключится на другое приложение, а затем вернется к вашему приложению. Например, пользователь выполняет поиск на экране, а затем нажимает кнопку «Домой» или отвечает на телефонный звонок — когда он возвращается на экран поиска, он ожидает увидеть ключевое слово и результаты поиска точно такими же, как и раньше.

В этом сценарии ваше приложение переводится в фоновый режим, и система делает все возможное, чтобы сохранить процесс приложения в памяти. Однако система может завершить процесс приложения, пока пользователь взаимодействует с другими приложениями. В таком случае уничтожается основная активность вместе со всем сохраненным в ней состоянием. Когда пользователь перезапускает приложение, экран неожиданно оказывается в чистом состоянии. Чтобы узнать больше о завершении процессов, см. раздел «Процессы и жизненный цикл приложения» .

Параметры сохранения состояния пользовательского интерфейса

Если ожидания пользователя относительно состояния пользовательского интерфейса не соответствуют стандартному поведению системы, необходимо сохранить и восстановить состояние пользовательского интерфейса, чтобы гарантировать, что инициированное системой уничтожение данных будет незаметным для пользователя.

Каждый из вариантов сохранения состояния пользовательского интерфейса различается по следующим параметрам, влияющим на пользовательский опыт:

ViewModel Сохраненное состояние Постоянное хранение
Место хранения в память в память на диске или в сети
Сохраняет изменения конфигурации Да Да Да
Выживает после завершения процесса, инициированного системой. Нет Да Да
Сохраняется после полного закрытия экрана пользователем/завершения работы программы ( finish() Нет Нет Да
Ограничения данных Сложные объекты — это хорошо, но пространство ограничено доступной памятью. только для примитивных типов и простых, небольших объектов, таких как String Ограничения связаны только с объемом дискового пространства или стоимостью/временем извлечения данных из сетевого ресурса.
Время чтения/записи быстрый (только доступ к памяти) Медленно (требует сериализации/десериализации) Медленно (требует доступа к диску или сетевых транзакций)

Используйте ViewModel для обработки изменений конфигурации.

ViewModel идеально подходит для хранения и управления данными, связанными с пользовательским интерфейсом, во время активного использования приложения пользователем. Он обеспечивает быстрый доступ к данным пользовательского интерфейса и помогает избежать повторной загрузки данных из сети или с диска при повороте экрана, изменении размера окна и других распространенных изменениях конфигурации. Чтобы узнать, как реализовать ViewModel, см. руководство по ViewModel .

ViewModel хранит данные в памяти, что означает, что их извлечение обходится дешевле, чем извлечение данных с диска или из сети. ViewModel связан с владельцем жизненного цикла, например, с пунктом назначения навигации или действием. Он остается в памяти при изменении конфигурации, и система автоматически связывает ViewModel с новым экземпляром владельца жизненного цикла, который возникает в результате изменения конфигурации.

В отличие от сохраненного состояния, ViewModel уничтожается при завершении процесса, инициированном системой. Для перезагрузки данных после завершения процесса, инициированного системой, в ViewModel используйте API SavedStateHandle . В качестве альтернативы, если данные связаны с пользовательским интерфейсом и не требуют хранения в ViewModel, используйте rememberSerializable . Для примитивных типов данных или сценариев, где не требуется использовать @Serializable , используйте rememberSaveable . Если данные являются данными приложения , то лучше сохранить их на диск.

Если у вас уже есть решение для хранения состояния пользовательского интерфейса в оперативной памяти при изменении конфигурации, вам, возможно, не понадобится использовать ViewModel.

Используйте сохраненное состояние в качестве резервной копии для обработки завершения процессов, инициированного системой.

API-интерфейсы, такие как rememberSerializable и rememberSaveable в Compose и SavedStateHandle в ViewModel, хранят данные, необходимые для перезагрузки состояния пользовательского интерфейса, если система уничтожает и впоследствии создает компонент заново. Для более эффективной обработки сложных структур данных SavedStateHandle поддерживает сериализацию Kotlinx через расширение saved {} , что позволяет беспрепятственно сохранять и восстанавливать типобезопасные объекты наряду со стандартными примитивными типами. Чтобы узнать, как реализовать сохранение состояния с помощью rememberSaveable , см. раздел «Состояние и Jetpack Compose» .

Сохраненные пакеты состояния сохраняются как при изменении конфигурации, так и при завершении процесса, но их возможности ограничены объемом памяти и скоростью, поскольку различные API сериализуют данные. Сериализация может потреблять много памяти, если сериализуемые объекты сложны. Поскольку этот процесс происходит в основном потоке во время изменения конфигурации, длительная сериализация может привести к выпадению кадров и визуальным зависаниям.

Не используйте сохраненное состояние для хранения больших объемов данных, таких как растровые изображения, или сложных структур данных, требующих длительной сериализации или десериализации. Вместо этого храните только примитивные типы и простые, небольшие объекты, такие как String . Таким образом, используйте сохраненное состояние для хранения минимального необходимого объема данных, например, идентификатора, чтобы восстановить данные, необходимые для возврата пользовательского интерфейса в предыдущее состояние в случае отказа других механизмов сохранения. Большинство приложений должны реализовать это для обработки завершения процесса, инициированного системой.

В зависимости от сценариев использования вашего приложения, вам может вообще не понадобиться сохранять состояние. Например, браузер может вернуть пользователя на ту же самую веб-страницу, которую он просматривал перед закрытием браузера. Если ваше приложение ведет себя таким образом, вы можете отказаться от сохранения состояния и вместо этого сохранять все данные локально.

Кроме того, при открытии активности из интента пакет дополнительных данных передается активности как при изменении конфигурации, так и при восстановлении активности системой. Если при запуске активности в качестве дополнительных данных интента передаются данные состояния пользовательского интерфейса, например, поисковый запрос, вы можете использовать пакет дополнительных данных вместо пакета сохраненного состояния. Подробнее о дополнительных данных интента см. в разделах «Интенты» и «Фильтры интентов» .

В любом из этих сценариев вам все равно следует использовать ViewModel , чтобы избежать траты ресурсов на перезагрузку данных из базы данных при изменении конфигурации.

В случаях, когда сохраняемые данные пользовательского интерфейса просты и невелики по объему, для сохранения данных состояния можно использовать только API сохранения состояния.

Подключитесь к сохраненному состоянию с помощью SavedStateRegistry.

Начиная с Fragment 1.1.0 или его транзитивной зависимости Activity 1.0.0 , компоненты пользовательского интерфейса, такие как ComponentActivity , реализуют SavedStateRegistryOwner и предоставляют объект SavedStateRegistry , привязанный к этому компоненту. SavedStateRegistry позволяет компонентам подключаться к сохраненному состоянию для его использования или внесения в него изменений. Например, модуль Saved State для 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 ) или компонуемый объект (используя rememberSerializable или rememberSaveable ) автоматически восстанавливают запрос. Если компонуемый объект восстанавливает запрос, он передает его ViewModel . ViewModel видит, что у него нет кэшированных результатов поиска, и делегирует загрузку результатов поиска, используя заданный поисковый запрос.
  • Пользовательский интерфейс пересоздается после изменения конфигурации. Поскольку экземпляр ViewModel не был уничтожен, вся информация в ViewModel кэшируется в памяти, и ей не нужно повторно обращаться к базе данных.

Дополнительные ресурсы

Чтобы узнать больше о сохранении состояний пользовательского интерфейса, ознакомьтесь со следующими ресурсами.

Кодлабс

Просмотры контента

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}