Состояние в приложении — это любое значение, которое может изменяться со временем. Это очень широкое определение, охватывающее всё, от базы данных Room до переменной в классе.
Все приложения для Android отображают состояние пользователю. Вот несколько примеров отображения состояния в приложениях Android:
- Панель быстрого доступа (Snackbar), отображающая сообщение, когда не удаётся установить сетевое соединение.
- Запись в блоге и соответствующие комментарии.
- При нажатии пользователем на кнопки появляется анимация пульсации.
- Наклейки, которые пользователь может рисовать поверх изображения.
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.
Чтобы узнать больше о первоначальном и повторном создании композиции, см. раздел «Мышление в процессе сочинения» .
Состояние в составных элементах
Компонуемые функции могут использовать API remember для хранения объекта в памяти. Значение, вычисленное с помощью remember сохраняется в композиции во время первоначальной композиции, а сохраненное значение возвращается во время рекомпозиции. remember может использоваться для хранения как изменяемых, так и неизменяемых объектов.
mutableStateOf создает наблюдаемый объект MutableState<T> , который является наблюдаемым типом, интегрированным со средой выполнения Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Любые изменения в расписаниях value , перекомпозиция любых компонуемых функций, считывающих value .
Существует три способа объявить объект MutableState в составном объекте:
-
val mutableState = remember { mutableStateOf(default) } -
var value by remember { mutableStateOf(default) } -
val (value, setValue) = remember { mutableStateOf(default) }
Эти объявления эквивалентны и предоставляются в качестве синтаксического сахара для различных способов использования состояния. Вам следует выбрать то, которое позволит получить наиболее читаемый код в создаваемом вами компоненте.
Для использования синтаксиса 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> , чтобы компонуемые объекты могли автоматически перестраиваться при изменении состояния.
Создавайте корабли с функциями для получения State<T> из распространенных наблюдаемых типов, используемых в приложениях Android. Перед использованием этих интеграций добавьте соответствующие артефакты, как описано ниже:
Flow:collectAsStateWithLifecycle()collectAsStateWithLifecycle()собирает значения изFlowс учетом жизненного цикла, что позволяет вашему приложению экономить ресурсы. Он представляет собой последнее значение, полученное из ComposeState. Используйте этот API как рекомендуемый способ сбора данных из Flow в приложениях Android.В файле
build.gradleтребуется следующая зависимость (версия должна быть 2.6.0-beta01 или новее):
Котлин
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.4")
}
Классный
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.9.4"
}
collectAsStateпохож наcollectAsStateWithLifecycle, поскольку он также собирает значения изFlowи преобразует их в ComposeState.Используйте
collectAsStateдля платформенно-независимого кода вместоcollectAsStateWithLifecycle, который предназначен только для Android.Для
collectAsStateдополнительные зависимости не требуются, поскольку он доступен вcompose-runtime.observeAsState()начинает отслеживать этиLiveDataи представляет их значения черезState.В файле
build.gradleтребуется следующая зависимость :
Котлин
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.10.1")
}
Классный
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.10.1"
}
subscribeAsState()— это функции расширения, которые преобразуют реактивные потоки RxJava2 (например,Single,Observable,Completable) вStateCompose.В файле
build.gradleтребуется следующая зависимость :
Котлин
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.10.1")
}
Классный
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.10.1"
}
subscribeAsState()— это функции расширения, которые преобразуют реактивные потоки RxJava3 (например,Single,Observable,Completable) вStateCompose.В файле
build.gradleтребуется следующая зависимость :
Котлин
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.10.1")
}
Классный
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.10.1"
}
С сохранением состояния против без сохранения состояния
Композируемый объект, использующий remember для хранения объекта, создает внутреннее состояние, что делает его состоятельным . HelloContent — пример состоятельного композируемого объекта, поскольку он хранит и изменяет свое состояние name внутри себя. Это может быть полезно в ситуациях, когда вызывающей стороне не нужно контролировать состояние, и она может использовать его, не управляя им самостоятельно. Однако композируемые объекты с внутренним состоянием, как правило, менее пригодны для повторного использования и сложнее тестируются.
Композируемый объект без состояния — это композируемый объект, который не хранит никакого состояния. Простой способ добиться отсутствия состояния — использовать поднятие состояния (state hoisting) .
При разработке многократно используемых составных объектов часто возникает необходимость предоставить как версию с сохранением состояния, так и версию без сохранения состояния для одного и того же объекта. Версия с сохранением состояния удобна для тех, кому не важен сам состояние, а версия без сохранения состояния необходима для тех, кому нужно управлять состоянием или поднимать его.
Государственный подъем
В Compose перенос состояния (State hoisting) — это паттерн, при котором состояние передается вызывающей стороне компонуемого объекта, чтобы сделать его бессостоятельным. Общий паттерн переноса состояния в Jetpack Compose заключается в замене переменной состояния двумя параметрами:
-
value: T: текущее значение для отображения -
onValueChange: (T) -> Unit: событие, запрашивающее изменение значения, гдеT— предлагаемое новое значение.
Однако вы не ограничены событием onValueChange . Если для составного объекта требуются более специфические события, их следует определять с помощью лямбда-выражений.
Государство, поднятое таким образом, обладает рядом важных свойств:
- Единый источник истины: перемещая состояние вместо его дублирования, мы гарантируем наличие только одного источника истины. Это помогает избежать ошибок.
- Инкапсуляция: Только компонуемые объекты с сохранением состояния могут изменять своё состояние. Это полностью внутренняя структура.
- Возможность совместного использования: поднятое состояние может быть совместно использовано несколькими составными объектами. Если вам нужно прочитать
nameв другом составном объекте, поднятие состояния позволит вам это сделать. - Перехватываемые: вызывающие объекты, не сохраняющие состояние, могут решить игнорировать или изменять события до изменения состояния.
- Разделение зависимостей: состояние для компонуемых объектов без состояния может храниться где угодно. Например, теперь можно переместить
nameвViewModel.
В приведенном примере вы извлекаете name и событие onValueChange из HelloContent и перемещаете их вверх по дереву в составной объект 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 упрощает понимание компонуемого объекта, его повторное использование в различных ситуациях и тестирование. HelloContent независим от способа хранения его состояния. Это означает, что если вы изменяете или заменяете HelloScreen , вам не нужно менять реализацию HelloContent .

Схема, при которой состояние изменяется, а события — изменяется, называется однонаправленным потоком данных . В данном случае состояние изменяется от HelloScreen к HelloContent , а события — от HelloContent к HelloScreen . Следуя однонаправленному потоку данных, вы можете отделить компоненты, отображающие состояние в пользовательском интерфейсе, от частей приложения, которые хранят и изменяют состояние.
Для получения более подробной информации посетите страницу «Где поднять оборудование в штате» .
Восстановление состояния в Compose
API rememberSaveable работает аналогично remember , поскольку сохраняет состояние при повторной композиции, а также при повторном создании активности или процесса, используя механизм сохранения состояния экземпляра. Например, это происходит при повороте экрана.
Способы хранения состояния
Все типы данных, добавленные в Bundle , сохраняются автоматически. Если вы хотите сохранить данные, которые нельзя добавить в Bundle , есть несколько вариантов.
Отправить посылки
Простейшее решение — добавить к объекту аннотацию @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 или, в более общем плане, страницу « Владельцы состояния и состояние пользовательского интерфейса» в руководстве по архитектуре.
Повторный запуск запоминает вычисления при изменении клавиш
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 и используется в качестве фоновой краски для компонуемого 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, обратитесь к следующим дополнительным ресурсам.
Образцы
Кодлабс
Видео
Блоги
{% verbatim %}Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Разработка архитектуры пользовательского интерфейса Compose
- Сохранение состояния пользовательского интерфейса в Compose
- Побочные эффекты в Compose