Состояние в приложении – это любое значение, которое может меняться со временем. Это очень широкое определение, которое охватывает все, от базы данных Room до переменной в классе.
Все приложения Android отображают состояние пользователю. Несколько примеров состояния в приложениях Android:
- Всплывающее уведомление, которое появляется, когда не удается установить сетевое подключение.
- Запись в блоге и связанные с ней комментарии.
- Анимация в виде ряби на кнопках, которая воспроизводится при нажатии.
- Стикеры, которые пользователь может нарисовать поверх изображения.
Jetpack Compose помогает точно определить, где и как вы храните и используете состояние в приложении Android. В этом руководстве основное внимание уделяется связи между состоянием и composable-функциями, а также 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 работают композиция и рекомпозиция.
Начальная композиция. Создание композиции путем первого запуска composable-функций.
Рекомпозиция. Повторный запуск composable-функций для обновления композиции при изменении данных.
Чтобы узнать больше о начальной композиции и рекомпозиции, ознакомьтесь с разделом Мышление в Compose.
Состояние в composable-функциях
Composable-функции могут использовать API remember для хранения объекта в памяти. Значение, вычисленное remember, сохраняется в Composition во время начальной композиции, а сохраненное значение возвращается во время рекомпозиции. remember можно использовать для хранения как изменяемых, так и неизменяемых объектов.
mutableStateOf создает наблюдаемый объект MutableState<T>, который интегрирован со средой выполнения Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Любые изменения в value приводят к перекомпоновке всех composable-функций, которые считывают 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
Запомненное значение можно использовать в качестве параметра для других composable-функций или даже в качестве логики в операторах, чтобы изменить отображаемые composable-функции. Например, если вы не хотите показывать приветствие, когда имя не указано, используйте состояние в операторе 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. Для других значений можно передать собственный объект Saver.
Другие поддерживаемые типы статусов
Compose не требует использования MutableState<T> для хранения состояния; он поддерживает другие наблюдаемые типы. Прежде чем считывать другой наблюдаемый тип в Compose, необходимо преобразовать его в State<T>, чтобы composable-функции могли автоматически перекомпоновываться при изменении состояния.
В 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и преобразует их вStateCompose.Используйте
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 для хранения объекта, создает внутреннее состояние, то есть является функцией с состоянием. HelloContent – пример composable-функции с сохранением состояния, поскольку она хранит и изменяет свое состояние name внутри. Это может быть полезно в ситуациях, когда вызывающему абоненту не нужно контролировать состояние и он может использовать его, не управляя им самостоятельно. Однако composable-функции с внутренним состоянием, как правило, менее пригодны для повторного использования и их сложнее тестировать.
Stateless – это composable-функция, которая не хранит состояние. Обычно для этого используется передача состояния.
При разработке многократно используемых composable-функций часто требуется предоставить как версию с отслеживанием состояния, так и версию без отслеживания состояния одной и той же composable-функции. Версия с отслеживанием состояния удобна для вызывающих функций, которым не важно состояние, а версия без отслеживания состояния необходима для вызывающих функций, которым необходимо контролировать или поднимать состояние.
Передача состояния
Передача состояния в Compose – это шаблон перемещения состояния к вызывающей функции composable-функции, чтобы сделать ее без отслеживания состояния. Общий шаблон для передачи состояния в Jetpack Compose – замена переменной состояния двумя параметрами:
value: T– текущее значение, которое нужно показать.onValueChange: (T) -> Unit– событие, запрашивающее изменение значения, гдеT– предлагаемое новое значение.
Однако вы не ограничены onValueChange. Если для composable-функции подходят более конкретные события, их следует определить с помощью лямбда-выражений.
У состояния, поднятого таким образом, есть несколько важных свойств:
- Единый источник достоверных данных. Перемещая состояние, а не дублируя его, мы гарантируем, что у нас будет только один источник достоверных данных. Это помогает избежать ошибок.
- Инкапсуляция. Только composable-функции с сохранением состояния могут изменять свое состояние. Это полностью внутренний процесс.
- Общий доступ. Поднятое состояние можно использовать в нескольких composable-функциях. Если бы вы хотели прочитать
nameв другой composable-функции, то могли бы сделать это с помощью подъема. - Перехватываемость. Вызывающие функции composable-функций без сохранения состояния могут игнорировать или изменять события перед изменением состояния.
- Разделение. Состояние 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. Следуя однонаправленному потоку данных, вы можете отделить composable-функции, которые отображают состояние в пользовательском интерфейсе, от частей вашего приложения, которые хранят и изменяют состояние.
- Состояние должно быть поднято как минимум до ближайшего общего родителя всех composable-функций, которые используют это состояние (чтение).
- Состояние должно быть поднято как минимум до самого высокого уровня, на котором оно может быть изменено (запись).
- Если два состояния изменяются в ответ на одни и те же события, их следует поднять вместе.
Вы можете поднять состояние выше, чем требуют эти правила, но недостаточное поднятие состояния затрудняет или делает невозможным следование однонаправленному потоку данных.
Дополнительную информацию можно найти на странице Где размещать состояние.
Восстановление состояния в Compose
API rememberSaveable работает аналогично 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
Простой передачей состояния можно управлять в самих composable-функциях. Однако если объем отслеживаемого состояния увеличивается или возникает логика, которую необходимо выполнить в composable-функциях, рекомендуется делегировать обязанности по логике и состоянию другим классам – хранилищам состояния.
Обратите внимание, что в других материалах хранилища состояния также называются объектами поднятого состояния.
Дополнительную информацию можно найти в документации по передаче состояния в Compose или в руководстве по архитектуре на странице Хранилища состояния и состояние пользовательского интерфейса.
Повторно запускать вычисления remember при изменении ключей
remember API часто используется вместе с 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