Состояние приложения – это любое значение, которое может меняться со временем. Это очень широкое определение, которое охватывает все, от базы данных Room до переменной в классе.
Все приложения для Android показывают пользователю свой статус. Вот несколько примеров состояния в приложениях Android:
- Снекбар, который появляется, когда не удается установить сетевое подключение.
- Запись в блоге и связанные с ней комментарии.
- Анимация волн на кнопках, которая воспроизводится, когда пользователь нажимает на них.
- Стикеры, которые пользователь может нарисовать поверх изображения.
Jetpack Compose помогает вам явно указывать, где и как вы храните и используете состояние в приложении Android. В этом руководстве рассказывается о связи между состоянием и композициями, а также об API, которые Jetpack Compose предлагает для работы с состоянием.
Состояние и состав
Compose – декларативный фреймворк, поэтому обновить его можно, только вызвав ту же composable-функцию с новыми аргументами. Эти аргументы представляют состояние интерфейса. При каждом обновлении состояния происходит повторная композиция. В результате такие элементы, как TextField, не обновляются автоматически, как в императивных представлениях на основе XML. Чтобы composable-функция обновилась соответствующим образом, ей нужно явно сообщить о новом состоянии.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
Если вы запустите этот код и попытаетесь ввести текст, то увидите, что ничего не происходит. Это связано с тем, что параметр TextField не обновляется автоматически, а только при изменении параметра value. Это связано с тем, как в Compose работают композиция и рекомпозиция.
Первоначальная композиция. Создание композиции при первом запуске компонуемых функций.
Повторная композиция – повторный запуск функций, создающих композицию, для ее обновления при изменении данных.
Чтобы узнать больше о начальной и повторной композиции, ознакомьтесь со статьей Thinking in Compose.
Состояние в composable-функциях
Компонуемые функции могут использовать API remember для хранения объекта в памяти. Значение, вычисленное с помощью remember, сохраняется в Composition во время первоначальной композиции, а сохраненное значение возвращается во время повторной композиции. remember можно использовать для хранения как изменяемых, так и неизменяемых объектов.
mutableStateOf создает наблюдаемый объект MutableState<T>, который представляет собой тип, интегрированный со средой выполнения Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Любые изменения в value приводят к перекомпоновке расписания любых композитных функций, которые считывают value.
Объект MutableState в composable-функции можно объявить тремя способами:
val mutableState = remember { mutableStateOf(default) }var value by remember { mutableStateOf(default) }val (value, setValue) = remember { mutableStateOf(default) }
Эти объявления эквивалентны и предоставляются в качестве синтаксического сахара для разных способов использования состояния. Выбирайте тот, который позволяет написать наиболее понятный код в composable-функции, которую вы пишете.
Для синтаксиса делегирования by требуются следующие импорты:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Сохраненное значение можно использовать как параметр для других композируемых функций или даже как логику в операторах, чтобы изменять отображаемые композируемые функции. Например, если вы не хотите показывать приветствие, когда имя не указано, используйте состояние в операторе if:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
Хотя remember помогает сохранить состояние при повторной композиции, оно не сохраняется при изменении конфигурации. Для этого необходимо использовать синтаксис rememberSaveable. rememberSaveable автоматически сохраняет любое значение, которое можно сохранить в Bundle. Для других значений можно передать специальный объект заставки.
Другие поддерживаемые типы статусов
Compose не требует использовать MutableState<T> для хранения состояния. Он поддерживает другие наблюдаемые типы. Прежде чем использовать в Compose другой наблюдаемый тип, его нужно преобразовать в State<T>, чтобы при изменении состояния композиции автоматически перерисовывались.
Compose содержит функции для создания State<T> из распространенных наблюдаемых типов, используемых в приложениях Android. Прежде чем использовать эти интеграции, добавьте нужные артефакты, выполнив следующие действия:
Flow:collectAsStateWithLifecycle()collectAsStateWithLifecycle()собирает значения изFlowс учетом жизненного цикла, что позволяет приложению экономить ресурсы. Оно представляет собой последнее значение, полученное от функции ComposeState. Этот API рекомендуется использовать для сбора данных о потоках в приложениях Android.В файле
build.gradleдолжна быть указана следующая зависимость (версия 2.6.0-beta01 или более новая):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.11.0")
}
Яркий
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.11.0"
}
-
collectAsStateпохож наcollectAsStateWithLifecycle, поскольку также собирает значения изFlowи преобразует их в ComposeState.Используйте
collectAsStateдля кода, который можно использовать на разных платформах, а неcollectAsStateWithLifecycle, который предназначен только для Android.Для
collectAsStateне требуются дополнительные зависимости, поскольку он доступен вcompose-runtime. -
observeAsState()начинает отслеживатьLiveDataи представляет его значения с помощьюState.В файле
build.gradleдолжна быть указана следующая зависимость:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.12.1")
}
Яркий
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.12.1"
}
-
subscribeAsState()– это функции-расширения, которые преобразуют реактивные потоки RxJava2 (например,Single,Observable,Completable) вStateCompose.В файле
build.gradleдолжна быть указана следующая зависимость:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.12.1")
}
Яркий
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.12.1"
}
-
subscribeAsState()– это функции расширения, которые преобразуют реактивные потоки RxJava3 (например,Single,Observable,Completable) вStateCompose.В файле
build.gradleдолжна быть указана следующая зависимость:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.12.1")
}
Яркий
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.12.1"
}
С отслеживанием состояния и без него
Composable-функция, в которой для хранения объекта используется remember, создает внутреннее состояние, поэтому такая функция называется composable-функцией с отслеживанием состояния. HelloContent – пример composable-функции с отслеживанием состояния, поскольку она хранит и изменяет свое состояние name. Это может быть полезно, когда вызывающему абоненту не нужно контролировать состояние и он может использовать его, не управляя им самостоятельно. Однако компоненты с внутренним состоянием обычно сложнее тестировать и использовать повторно.
Composable-функция без отслеживания состояния – это composable-функция, которая не хранит состояние. Один из распространенных способов сделать компонент без состояния – использовать подъем состояния.
При разработке многократно используемых composable-функций часто требуется предоставить как версию с отслеживанием состояния, так и версию без отслеживания состояния. Версия с отслеживанием состояния удобна для вызывающих функций, которым не нужно знать состояние, а версия без отслеживания состояния необходима для вызывающих функций, которым нужно контролировать или поднимать состояние.
Передача состояния
Подъем состояния в Compose – это перенос состояния в вызывающий компонент, чтобы сделать компонент без состояния. Общий принцип поднятия состояния в Jetpack Compose заключается в том, чтобы заменить переменную состояния двумя параметрами:
value: T– текущее значение для показа.onValueChange: (T) -> Unit– событие, которое запрашивает изменение значения, гдеT– новое значение.
Однако вы не ограничены доменом onValueChange. Если для composable-функции подходят более конкретные события, их следует определить с помощью лямбда-выражений.
Состояние, поднятое таким образом, имеет следующие важные свойства:
- Единый источник достоверных данных. Перемещая состояние вместо его дублирования, мы обеспечиваем наличие только одного источника достоверных данных. Это поможет избежать ошибок.
- Инкапсулированность. Изменять состояние могут только композиции с состоянием. Это полностью внутренний процесс.
- Возможность совместного использования. Поднятое состояние можно использовать в нескольких композируемых функциях. Если вы хотите прочитать
nameв другом компоненте, то сможете сделать это с помощью подъема. - Перехватываемые. Вызывающие функции без состояния могут игнорировать или изменять события до изменения состояния.
- Отдельное состояние. Состояние для composable-функций без отслеживания состояния может храниться где угодно. Например, теперь можно переместить
nameвViewModel.
В примере вы извлекаете name и onValueChange из HelloContent и перемещаете их вверх по дереву в composable-функцию HelloScreen, которая вызывает HelloContent.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
Вынося состояние из HelloContent, вы упрощаете рассуждения о composable-функции, ее повторное использование в разных ситуациях и тестирование. HelloContent не зависит от того, как хранится его состояние. Это означает, что если вы измените или замените HelloScreen, вам не придется менять способ реализации HelloContent.
Шаблон, в котором состояние уменьшается, а количество событий увеличивается, называется однонаправленным потоком данных. В этом случае состояние переходит с HelloScreen на HelloContent, а события – с HelloContent на HelloScreen. Однонаправленный поток данных позволяет отделить компоненты, которые отображают состояние в интерфейсе, от частей приложения, которые хранят и изменяют состояние.
- Состояние должно быть поднято как минимум до наименьшего общего родительского элемента всех композиций, которые используют состояние (читают его).
- Состояние должно быть поднято как минимум до самого высокого уровня, на котором его можно изменить (записать).
- Если два состояния изменяются в ответ на одни и те же события, их следует поднимать вместе.
Вы можете поднять состояние выше, чем требуется по этим правилам, но если опустить его ниже, то будет сложно или невозможно отслеживать однонаправленный поток данных.
Подробнее о том, куда переносить состояние…
Восстановление состояния в Compose
rememberSaveable API работает так же, как remember, поскольку сохраняет состояние при повторной композиции, а также при воссоздании активности или процесса с помощью сохраненного состояния экземпляра. Например, это происходит при повороте экрана.
Способы хранения состояния
Все типы данных, добавленные в Bundle, сохраняются автоматически. Если вы хотите сохранить что-то, что нельзя добавить в Bundle, есть несколько вариантов.
Parcelize
Самое простое решение – добавить к объекту аннотацию @Parcelize. Объект становится пакетируемым и может быть включен в пакет. Например, этот код создает тип данных City, который можно передавать между компонентами, и сохраняет его в состоянии.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
Если по какой-либо причине @Parcelize не подходит, вы можете использовать mapSaver, чтобы определить собственное правило преобразования объекта в набор значений, которые система может сохранить в Bundle.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
Чтобы не определять ключи для карты, можно также использовать listSaver и его индексы в качестве ключей:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Хранилища состояния в Compose
Простое поднятие состояния можно реализовать в самих функциях, созданных с помощью Compose. Однако если объем данных, которые нужно отслеживать, увеличивается или в компонуемых функциях возникает сложная логика, рекомендуется делегировать логику и ответственность за состояние другим классам – хранителям состояния.
Обратите внимание, что в других материалах держатели состояния также называются поднятыми объектами состояния.
Подробнее о подъеме состояния в Compose и хранителях состояния и состоянии интерфейса рассказывается в руководстве по архитектуре.
Повторно запускать расчеты запоминания при изменении ключей
API remember часто используется вместе с MutableState:
var name by remember { mutableStateOf("") }
В этом случае использование функции remember позволяет сохранить значение MutableState при повторном создании.
В общем случае функция remember принимает параметр лямбда-функции calculation. При первом запуске функции remember вызывается лямбда-функция calculation и сохраняется ее результат. Во время повторной композиции remember возвращает последнее сохраненное значение.
Помимо кэширования состояния, remember можно использовать для хранения любого объекта или результата операции в композиции, инициализация или вычисление которого требует больших затрат. Вам может не понадобиться повторять это вычисление при каждой рекомпозиции.
Примером может служить создание объекта ShaderBrush, которое требует значительных ресурсов:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember сохраняет значение, пока оно не будет удалено из композиции. Однако есть способ сделать кешированное значение недействительным. API remember также принимает параметр key или keys. Если какой-либо из этих ключей изменится, при следующем повторном создании функцииremember кеш будет признан недействительным и блок лямбда-выражения будет выполнен снова. Этот механизм позволяет контролировать время существования объекта в композиции. Расчет остается действительным, пока не изменятся входные данные, а не пока запомненное значение не будет удалено из композиции.
Ниже приведены примеры того, как работает этот механизм.
В этом фрагменте кода создается объект ShaderBrush, который используется в качестве фона для composable-функции Box. remember сохраняет экземпляр ShaderBrush, поскольку его воссоздание требует больших затрат, как уже говорилось ранее. remember принимает avatarRes в качестве параметра key1, который представляет собой выбранное фоновое изображение. Если изменить avatarRes, кисть будет перекомпонована с новым изображением и снова применена к Box. Это может произойти, когда пользователь выбирает другое изображение в качестве фона.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
В следующем фрагменте кода состояние перенесено в обычный класс для хранения состояния
MyAppState. Он предоставляет функцию rememberMyAppState для инициализации экземпляра класса с помощью remember. Предоставление таких функций для создания экземпляра, который сохраняется после перекомпоновки, – распространенный подход в Compose. Функция rememberMyAppState получает значение windowSizeClass, которое используется в качестве параметра key для функции remember. Если этот параметр изменится, приложению нужно будет воссоздать класс держателя простого состояния с последним значением. Это может произойти, например, если пользователь повернет устройство.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose использует реализацию метода equals класса, чтобы определить, изменился ли ключ, и аннулировать сохраненное значение.
Как хранить состояние с помощью ключей после повторной композиции
API rememberSaveable – это оболочка для remember, которая может хранить данные в Bundle. Этот API позволяет сохранять состояние не только при повторной композиции, но и при воссоздании активности и завершении процесса, инициированном системой.
rememberSaveable получает параметры input для той же цели, что и remember получает keys. Кеш становится недействительным, если изменяется хотя бы один из входных аргументов. При следующем повторном создании функции rememberSaveable снова выполнит лямбда-блок вычислений.
В приведенном ниже примере rememberSaveable хранит userTypedQuery, пока не изменится typedQuery:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
Подробнее…
Чтобы узнать больше о состоянии и Jetpack Compose, ознакомьтесь с дополнительными ресурсами.
Образцы
Практические работы
Видео
Блоги
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Архитектура интерфейса Compose
- Сохранение состояния интерфейса в Compose
- Побочные эффекты в Compose