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

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

Любое приложение Android может потерять состояние пользовательского интерфейса из-за активности или воссоздания процесса. Эта потеря состояния может произойти из-за следующих событий:

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

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

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

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

В следующем фрагменте rememberSaveable используется для хранения одного логического состояния элемента пользовательского интерфейса:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Рисунок 1 . Пузырь сообщений чата расширяется и сворачивается при нажатии.

showDetails — это логическая переменная, которая сохраняет данные о том, свернуто или развернуто окно чата.

rememberSaveable сохраняет состояние элемента пользовательского интерфейса в Bundle с помощью механизма сохранения состояния экземпляра.

Он может автоматически сохранять примитивные типы в пакете. Если ваше состояние хранится в типе, который не является примитивным, например в классе данных, вы можете использовать различные механизмы хранения, такие как использование аннотации Parcelize , использование API-интерфейсов Compose, таких как listSaver и mapSaver , или реализация пользовательского класса сохранения, расширяющего Compose Runtime Saver сорт. См. раздел «Способы хранения документации состояния» , чтобы узнать больше об этих методах.

В следующем фрагменте API rememberLazyListState Compose сохраняет LazyListState , который состоит из состояния прокрутки LazyColumn или LazyRow , с помощью rememberSaveable . Он использует LazyListState.Saver — настраиваемую заставку, способную сохранять и восстанавливать состояние прокрутки. После воссоздания действия или процесса (например, после изменения конфигурации, например изменения ориентации устройства), состояние прокрутки сохраняется.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Лучшая практика

rememberSaveable использует Bundle для хранения состояния пользовательского интерфейса, которое используется другими API, которые также записывают в него данные, например вызовы onSaveInstanceState() в вашей активности. Однако размер этого Bundle ограничен, и хранение больших объектов может привести к исключениям TransactionTooLarge во время выполнения. Это может быть особенно проблематично в приложениях с одним Activity , где один и тот же Bundle используется во всем приложении.

Чтобы избежать сбоя такого типа, не следует хранить в бандле большие сложные объекты или списки объектов .

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

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

Проверка восстановления состояния

Вы можете убедиться, что состояние, сохраненное с помощью rememberSaveable в элементах Compose, правильно восстанавливается при воссоздании действия или процесса. Для этого существуют специальные API, например StateRestorationTester . Чтобы узнать больше, ознакомьтесь с документацией по тестированию .

Бизнес-логика

Если состояние вашего элемента пользовательского интерфейса переносится в ViewModel поскольку это требуется бизнес-логикой, вы можете использовать API-интерфейсы ViewModel .

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

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

Лучшая практика

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

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

API SavedStateHandle

SavedStateHandle имеет различные API для хранения состояния элемента пользовательского интерфейса, в частности:

Составление State saveable()
StateFlow getStateFlow()

Составление State

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

API saveable поддерживает примитивные типы «из коробки» и получает параметр stateSaver для использования пользовательских хранителей, как и rememberSaveable() .

В следующем фрагменте message сохраняет типы пользовательского ввода в TextField :

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

Дополнительную информацию об использовании saveable API см. в документации SavedStateHandle .

StateFlow

Используйте getStateFlow() для сохранения состояния элемента пользовательского интерфейса и использования его как потока из SavedStateHandle . StateFlow доступен только для чтения, и API требует, чтобы вы указали ключ, чтобы вы могли заменить поток для создания нового значения. С помощью настроенного вами ключа вы можете получить StateFlow и получить последнее значение.

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

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

Каждый раз, когда пользователь выбирает новый тип фильтра, вызывается setFiltering . Это сохраняет новое значение в SavedStateHandle , хранящееся с ключом _CHANNEL_FILTER_SAVED_STATE_KEY_ . savedFilterType — это поток, отправляющий последнее значение, сохраненное в ключе. filteredChannels подписывается на поток для выполнения фильтрации каналов.

Дополнительную информацию об API getStateFlow() см. в документации SavedStateHandle .

Краткое содержание

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

Событие Логика пользовательского интерфейса Бизнес-логика в ViewModel
Изменения конфигурации rememberSaveable Автоматический
Смерть процесса, инициированная системой rememberSaveable SavedStateHandle

Используемый API зависит от того, где хранится состояние, и от требуемой логики. Для состояния, которое используется в логике пользовательского интерфейса , используйте rememberSaveable . Если состояние, используемое в бизнес-логике , хранится в ViewModel , сохраните его с помощью SavedStateHandle .

Вам следует использовать API-интерфейсы пакета ( rememberSaveable и SavedStateHandle ) для хранения небольших объемов состояния пользовательского интерфейса. Эти данные — минимум, необходимый для восстановления пользовательского интерфейса в предыдущее состояние вместе с другими механизмами хранения. Например, если вы сохраните идентификатор профиля, который просматривал пользователь, в пакете, вы можете получить тяжелые данные, такие как сведения о профиле, с уровня данных.

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

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