Состояние в приложении – это любое значение, которое может меняться со временем. Это очень широкое определение, которое охватывает всё – от базы данных 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-функций для обновления композиции при изменении данных.
Подробнее о начальной композиции и рекомпозиции рассказывается в статье Thinking in Compose (Мышление в 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>, чтобы составные элементы могли автоматически перекомпоновываться при изменении состояния.
В 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 для хранения объекта, создает внутреннее состояние, то есть является composable-функцией с сохранением состояния. HelloContent – пример composable-функции с сохранением состояния, поскольку она хранит и изменяет свое состояние name внутри. Это может быть полезно в ситуациях, когда вызывающему абоненту не нужно контролировать состояние и он может использовать его, не управляя им самостоятельно. Однако composable-функции с внутренним состоянием, как правило, менее пригодны для повторного использования и их сложнее тестировать.
Stateless – это composable-функция, которая не содержит состояния. Обычно для этого используется передача состояния.
При разработке многократно используемых composable-функций часто требуется предоставить как версию с отслеживанием состояния, так и версию без отслеживания состояния одной и той же composable-функции. Версия с отслеживанием состояния удобна для вызывающих функций, которым не важно состояние, а версия без отслеживания состояния необходима для вызывающих функций, которым необходимо контролировать или поднимать состояние.
Передача состояния
Передача состояния в Compose – это шаблон перемещения состояния в вызывающую функцию composable-функции, чтобы сделать ее без отслеживания состояния. Общий шаблон для передачи состояния в Jetpack Compose заключается в замене переменной состояния двумя параметрами:
value: T– текущее значение, которое нужно показать.onValueChange: (T) -> Unit– событие, которое запрашивает изменение значения, гдеT– предлагаемое новое значение.
Однако вы не ограничены onValueChange. Если для composable-функции подходят более конкретные события, их следует определить с помощью лямбда-выражений.
State that is hoisted this way has some important properties:
- Единый источник достоверных данных. Перемещая состояние, а не дублируя его, мы гарантируем, что у нас будет только один источник достоверных данных. Это помогает избежать ошибок.
- Инкапсулированность. Изменять состояние могут только composable-функции с состоянием. Это полностью внутренний процесс.
- Общий доступ. Поднятое состояние можно использовать в нескольких composable-функциях. Если вы хотите прочитать
nameв другой 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