Сроки жизни состояний в Compose

В Jetpack Compose компонуемые функции часто хранят состояние с помощью функции remember . Запомненные значения могут быть повторно использованы при повторной композиции, как объяснено в разделе «Состояние и Jetpack Compose» .

Хотя remember служит инструментом для сохранения значений при повторной композиции, состояние часто должно сохраняться и после завершения композиции. На этой странице объясняется разница между API remember , retain , rememberSaveable и rememberSerializable , когда следует выбирать тот или иной API, а также каковы лучшие практики управления запомненными и сохраняемыми значениями в Compose.

Выберите правильный срок службы

В Compose существует несколько функций, которые можно использовать для сохранения состояния между композициями и за их пределами: remember , retain , rememberSaveable и rememberSerializable . Эти функции различаются по времени жизни и семантике, и каждая из них подходит для хранения определенных типов состояния. Различия описаны в следующей таблице:

remember

retain

rememberSaveable , rememberSerializable

Ценности выдерживают рекомпозицию?

Сохранятся ли ценности в условиях активного отдыха?

Всегда будет возвращаться один и тот же экземпляр ( === ).

Будет возвращен эквивалентный объект ( == ), возможно, десериализованная копия.

Ценности сохраняются после завершения процесса?

Поддерживаемые типы данных

Все

Не следует ссылаться на объекты, которые могут быть скомпрометированы в случае уничтожения активности.

Должен быть сериализуемым
(либо с помощью пользовательского Saver , либо с помощью kotlinx.serialization )

Варианты использования

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

remember

remember — наиболее распространенный способ хранения состояния в Compose. При первом вызове remember выполняется заданное вычисление, которое запоминается , то есть Compose сохраняет его для последующего повторного использования компонуемым объектом. При перекомпоновке компонуемого объекта его код выполняется снова, но любые вызовы remember возвращают значения из предыдущей компоновки вместо повторного выполнения вычисления.

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

Когда запомненное значение больше не используется, оно забывается , и его запись удаляется. Запомненные значения забываются, когда они удаляются из иерархии композиции (включая случаи, когда значение удаляется и добавляется заново для перемещения в другое место без использования key composable или MovableContent ) или вызываются с другими параметрами key .

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

  • Создание внутренних объектов состояния, таких как положение прокрутки или состояние анимации.
  • Избежать дорогостоящего пересоздания объектов при каждой перекомпозиции

Однако следует избегать:

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

rememberSaveable и rememberSerializable

Функции rememberSaveable и rememberSerializable основаны на remember . Они обладают самым длительным сроком жизни среди функций мемоизации, рассмотренных в этом руководстве. Помимо позиционной мемоизации объектов при перекомпозициях, они также могут сохранять значения, чтобы их можно было восстановить при повторном создании активности, в том числе после изменений конфигурации и завершения процесса (когда система завершает процесс вашего приложения, пока он находится в фоновом режиме, обычно либо для освобождения памяти для приложений переднего плана, либо если пользователь отзывает разрешения у вашего приложения во время его работы).

Функция rememberSerializable работает аналогично rememberSaveable , но автоматически поддерживает сохранение сложных типов, сериализуемых с помощью библиотеки kotlinx.serialization . Выберите rememberSerializable если ваш тип помечен (или может быть помечен) аннотацией @Serializable , и rememberSaveable во всех остальных случаях.

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

Обратите внимание, что rememberSaveable и rememberSerializable сохраняют свои мемоизированные значения, сериализуя их в объект Bundle . Это приводит к двум последствиям:

  • Значения, которые вы кэшируете, должны быть представимы одним или несколькими из следующих типов данных: примитивы (включая Int , Long , Float , Double ), String или массивы любого из этих типов.
  • При восстановлении сохраненного значения будет создан новый экземпляр, равный ( == ), но не тот же === , что и в композиции, использованной ранее.

Для хранения более сложных типов данных без использования kotlinx.serialization можно реализовать собственный Saver для сериализации и десериализации объекта в поддерживаемые типы данных. Обратите внимание, что Compose понимает распространенные типы данных, такие как State , List , Map , Set и т. д., и автоматически преобразует их в поддерживаемые типы. Ниже приведен пример Saver для класса Size . Он реализован путем упаковки всех свойств класса Size в список с помощью listSaver .

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

API retain занимает промежуточное положение между remember и rememberSaveable / rememberSerializable с точки зрения длительности мемоизации значений. Он назван по-разному, потому что сохраняемые значения также проходят другой жизненный цикл, чем их запоминаемые аналоги.

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

Взамен более короткого жизненного цикла по сравнению с rememberSaveable , retain позволяет сохранять значения, которые нельзя сериализовать, такие как лямбда-выражения, потоки и большие объекты, например, растровые изображения. Например, вы можете использовать retain для управления медиаплеером (таким как ExoPlayer), чтобы предотвратить прерывание воспроизведения мультимедиа во время изменения конфигурации.

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retain против ViewModel

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

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

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

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

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

retain

ViewModel

Определение масштаба исследования

Нет общих значений; каждое значение сохраняется и связывается с определенной точкой в ​​иерархии композиции. Сохранение одного и того же типа в другом месте всегда приводит к созданию нового экземпляра.

ViewModel — это синглтоны внутри ViewModelStore

Разрушение

При окончательном выходе из иерархии композиции

Когда ViewModelStore очищается или уничтожается

Дополнительные функции

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

Встроенная coroutineScope , поддержка SavedStateHandle , возможность внедрения с помощью Hilt.

Принадлежит

RetainedValuesStore

ViewModelStore

Варианты использования

  • Сохранение значений, специфичных для пользовательского интерфейса, локально для отдельных составных экземпляров.
  • Отслеживание показов, возможно, с помощью RetainedEffect
  • Строительный блок для определения пользовательского компонента архитектуры, аналогичной ViewModel.
  • Вынесение взаимодействия между пользовательским интерфейсом и слоями данных в отдельный класс — как для организации кода, так и для тестирования.
  • Преобразование Flow в объекты State и вызов функций приостановки, которые не должны прерываться при изменении конфигурации.
  • Совместное использование состояний на больших участках пользовательского интерфейса, например, на целых экранах.
  • Взаимодействие с View

Объедините retain и rememberSaveable или rememberSerializable

Иногда объекту требуется гибридный жизненный цикл, сочетающий в себе свойства retained и rememberSaveable или rememberSerializable . Это может указывать на то, что ваш объект должен быть ViewModel , который поддерживает сохранение состояния, как описано в руководстве по модулю Saved State для ViewModel .

Можно одновременно использовать retain и rememberSaveable или rememberSerializable . Правильное сочетание обоих жизненных циклов значительно усложняет задачу. Мы рекомендуем использовать этот шаблон в рамках более сложных и пользовательских архитектурных шаблонов и только в том случае, если выполняются все следующие условия:

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

Когда выполняются все эти условия, мы рекомендуем разделить ваш класс на три части: сохраненные данные, данные, находящиеся в хранилище, и объект-«посредник», который не имеет собственного состояния и делегирует обновление состояния объектам, находящимся в хранилище, и сохраненным данным. Эта схема принимает следующую форму:

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

Разделение состояния по времени жизни делает разделение обязанностей и хранения очень явным. Заранее предусмотрено, что данные сохранения нельзя изменять с помощью механизма сохранения данных, поскольку это предотвращает ситуацию, когда попытка обновления данных сохранения предпринимается, когда пакет savedInstanceState уже захвачен и не может быть обновлен. Это также позволяет тестировать сценарии воссоздания, проверяя конструкторы без вызова Compose или имитации воссоздания Activity.

Полный пример реализации этого шаблона можно найти в файле ( RetainAndSaveSample.kt ).

Позиционная мемоизация и адаптивная компоновка

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

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

Для стандартных компонентов, таких как ListDetailPaneScaffold и NavDisplay (из Jetpack Navigation 3), это не проблема, и ваше состояние будет сохраняться при изменении макета. Для пользовательских компонентов, адаптирующихся к различным форм-факторам, убедитесь, что состояние не изменяется при изменении макета, выполнив одно из следующих действий:

  • Убедитесь, что объекты, сохраняющие состояние и компонуемые в иерархии, всегда вызываются в одном и том же месте. Реализуйте адаптивную компоновку, изменяя логику компоновки вместо перемещения объектов в иерархии композиции.
  • Используйте MovableContent для корректного перемещения составных объектов с сохранением состояния. Экземпляры MovableContent способны перемещать запомненные и сохраненные значения из старых мест в новые.

Помните о функциях завода.

Хотя интерфейсы Compose состоят из компонуемых функций, в создании и организации композиции участвует множество объектов. Наиболее распространенный пример — сложные компонуемые объекты, которые определяют собственное состояние, например, LazyList , который принимает LazyListState .

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

  • Добавьте префикс remember к имени функции. При желании, если реализация функции зависит от retained объекта, и API никогда не будет развиваться, чтобы использовать другой вариант remember , используйте вместо этого префикс retain .
  • Используйте rememberSaveable или rememberSerializable если выбрано сохранение состояния и есть возможность написать корректную реализацию Saver .
  • Избегайте побочных эффектов или инициализации значений на основе CompositionLocal , которые могут быть неактуальны для использования. Помните, что место создания состояния может отличаться от места его использования.

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}