В Jetpack Compose функции, которые можно использовать в композиции, часто хранят состояние с помощью функции remember. Значения, которые запоминаются, можно использовать повторно при перекомпозиции, как описано в разделе Состояние в Jetpack Compose.
Хотя remember позволяет сохранять значения между перерисовками, состояние часто должно существовать дольше, чем композиция. На этой странице рассказывается о различиях между API remember, retain, rememberSaveable и rememberSerializable, о том, когда какой из них следует использовать, а также о рекомендациях по управлению запомненными и сохраненными значениями в Compose.
Как выбрать правильный срок действия
В Compose есть несколько функций, которые позволяют сохранять состояние между композициями и не только: remember, retain, rememberSaveable и rememberSerializable. Эти функции различаются по времени жизни и семантике и подходят для хранения разных типов данных. В таблице ниже приведены различия между ними.
|
|
|
|
|---|---|---|---|
Сохраняются ли значения при изменении композиции? |
✅ |
✅ |
✅ |
Сохраняются ли значения при воссоздании действий? |
❌ |
✅ Всегда возвращается один и тот же экземпляр ( |
✅ Будет возвращен эквивалентный объект ( |
Сохраняются ли значения после завершения процесса? |
❌ |
❌ |
✅ |
Поддерживаемые типы данных |
Все |
Нельзя ссылаться на объекты, которые будут удалены при уничтожении действия. |
Должен быть сериализуемым |
Примеры использования |
|
|
|
remember
remember – самый распространенный способ хранения состояния в Compose. Когда remember вызывается впервые, выполняется заданное вычисление и запоминается, то есть сохраняется Compose для дальнейшего использования в компоненте. Когда composable-функция перекомпоновывается, ее код выполняется снова, но все вызовы remember возвращают значения из предыдущей композиции, а не выполняют вычисления заново.
Каждый экземпляр функции, допускающей композицию, имеет собственный набор запомненных значений, который называется позиционным мемоизированием. Когда запомненные значения сохраняются для использования в последующих композициях, они привязываются к своей позиции в иерархии композиции. Если composable-функция используется в разных местах, каждый экземпляр в иерархии композиции имеет свой набор сохраненных значений.
Если запомненное значение больше не используется, оно забывается и запись о нем удаляется. Значения, которые были запомнены, забываются, когда они удаляются из иерархии композиции (в том числе когда значение удаляется и добавляется снова, чтобы переместить его в другое место без использования функции key или 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 лучше всего подходит для объектов, которые относятся к определенным экземплярам composable-функций и не требуют повторного использования или обмена между родственными composable-функциями. ViewModel – это хорошее место для хранения состояния интерфейса и выполнения фоновых задач, а retain – для хранения объектов, необходимых для работы интерфейса, например кешей, средств отслеживания показов и аналитики, зависимостей от AndroidView и других объектов, которые взаимодействуют с ОС Android или управляют сторонними библиотеками, такими как платежные системы или рекламные платформы.
Для опытных пользователей, которые разрабатывают архитектуру приложений, отличную от современной архитектуры приложений Android, retain можно использовать для создания собственного API, похожего на ViewModel. Хотя поддержка сопрограмм и сохраненного состояния не предлагается "из коробки", retain может служить строительным блоком для жизненного цикла таких ViewModel-подобных объектов с этими функциями, встроенными поверх. Подробные инструкции по созданию такого компонента выходят за рамки этого руководства.
|
|
|
|---|---|---|
Область действия |
Нет общих значений. Каждое значение сохраняется и связывается с определенной точкой в иерархии композиции. Сохранение того же типа в другом местоположении всегда действует на новый экземпляр. |
|
Уничтожение |
Как навсегда покинуть иерархию композиции |
Когда данные приложения " |
Дополнительные функции |
Может получать обратные вызовы, когда объект находится в иерархии композиции или нет. |
Встроенный |
Владелец |
|
|
Примеры использования |
|
|
Сочетание эмодзи retain и rememberSaveable или rememberSerializable
Иногда объекту требуется гибридный срок действия, сочетающий retained и rememberSaveable или rememberSerializable. Это может указывать на то, что ваш объект должен быть ViewModel, который может поддерживать сохраненное состояние, как описано в руководстве по модулю сохраненного состояния для ViewModel.
Можно использовать retain и rememberSaveable или rememberSerializable одновременно. Правильное сочетание обоих жизненных циклов – сложная задача.
Мы рекомендуем использовать этот шаблон в составе более сложных и настраиваемых архитектурных шаблонов и только в том случае, если выполняются все следующие условия:
- Вы определяете объект, состоящий из значений, которые необходимо сохранить (например, объект, отслеживающий ввод данных пользователем, и кеш в памяти, который нельзя записать на диск).
- Область действия вашего состояния ограничена composable-функцией, поэтому оно не подходит для области действия или времени жизни объекта-одиночки
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, чтобы корректно перемещать composable-функции с состоянием. ЭкземплярыMovableContentмогут переносить сохраненные значения из старых местоположений в новые.
Запомните заводские функции
Хотя интерфейсы Compose состоят из функций, при создании и организации композиции используется множество объектов. Самый распространенный пример – сложные составные объекты, которые определяют собственное состояние, например LazyList, принимающий LazyListState.
При определении объектов, ориентированных на Compose, мы рекомендуем создать функцию remember, чтобы задать желаемое поведение запоминания, включая срок жизни и ключевые входные данные. Это позволяет потребителям состояния уверенно создавать экземпляры в иерархии композиции, которые будут сохраняться и становиться недействительными в соответствии с ожиданиями. При определении composable-функции фабрики следуйте приведенным ниже рекомендациям.
- Перед названием функции добавьте префикс
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) } ) }