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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В отличие от сохраненного состояния экземпляра, модели представления уничтожаются во время завершения процесса, инициированного системой. Чтобы перезагрузить данные после инициированного системой завершения процесса в ViewModel, используйте API SavedStateHandle . В качестве альтернативы, если данные связаны с пользовательским интерфейсом и их не нужно хранить в ViewModel, используйте onSaveInstanceState() в системе View или rememberSaveable в Jetpack Compose. Если данные являются данными приложения , возможно, лучше сохранить их на диске.

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

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

Обратный вызов onSaveInstanceState() в системе View, rememberSaveable в Jetpack Compose и SavedStateHandle в ViewModels хранят данные, необходимые для перезагрузки состояния контроллера пользовательского интерфейса, такого как действие или фрагмент, если система уничтожает, а затем воссоздает этот контроллер. Чтобы узнать, как реализовать сохраненное состояние экземпляра с помощью onSaveInstanceState , см. раздел Сохранение и восстановление состояния активности в руководстве по жизненному циклу активности .

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

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

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

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

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

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

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

Начиная с Fragment 1.1.0 или его транзитивной зависимости Activity 1.0.0 , контроллеры пользовательского интерфейса, такие как Activity или Fragment , реализуют SavedStateRegistryOwner и предоставляют SavedStateRegistry , привязанный к этому контроллеру. SavedStateRegistry позволяет компонентам подключаться к сохраненному состоянию вашего контроллера пользовательского интерфейса, чтобы использовать его или вносить в него свой вклад. Например, модуль Saved State для ViewModel использует SavedStateRegistry для создания SavedStateHandle и предоставления его объектам ViewModel . Вы можете получить SavedStateRegistry из контроллера пользовательского интерфейса, вызвав getSavedStateRegistry() .

Компоненты, которые способствуют сохранению состояния, должны реализовать 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)
    }
}

Ява

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)
    ...
}

Ява

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 кэшируется в памяти, и ему не нужно повторно запрашивать базу данных.

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

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

Блоги

{% дословно %} {% дословно %} {% дословно %} {% дословно %}