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