Сохранение состояния пользовательского интерфейса в 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 Compose rememberLazyListState сохраняет 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

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

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

Для получения дополнительной информации об использовании API saveable см. документацию 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 ). Эти данные — минимально необходимые для восстановления пользовательского интерфейса до его предыдущего состояния, наряду с другими механизмами хранения. Например, если вы храните в пакете идентификатор профиля, который просматривал пользователь, вы можете получать большие объемы данных, такие как сведения о профиле, из слоя данных.

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

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