State и Jetpack Compose

Состояние в приложении — это любое значение, которое может меняться со временем. Это очень широкое определение, охватывающее все: от базы данных Room до переменной в классе.

Все приложения Android отображают состояние пользователю. Несколько примеров состояния в приложениях для Android:

  • Снэкбар, который показывает, когда не удается установить сетевое соединение.
  • Сообщение в блоге и связанные с ним комментарии.
  • Пульсирующая анимация на кнопках, которая воспроизводится, когда пользователь нажимает на них.
  • Стикеры, которые пользователь может нарисовать поверх изображения.

Jetpack Compose помогает вам четко указать, где и как вы храните и используете состояние в приложении Android. В этом руководстве основное внимание уделяется связи между состоянием и составными объектами, а также API-интерфейсам, которые Jetpack Compose предлагает для более простой работы с состоянием.

Состояние и состав

Compose является декларативным, и поэтому единственный способ его обновления — вызвать тот же составной объект с новыми аргументами. Эти аргументы представляют состояние пользовательского интерфейса. Каждый раз, когда состояние обновляется, происходит рекомпозиция . В результате такие вещи, как TextField не обновляются автоматически, как в императивных представлениях на основе XML. Компонуемому объекту необходимо явно сообщить новое состояние, чтобы он мог соответствующим образом обновиться.

@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> , чтобы составные объекты могли автоматически перекомпоновываться при изменении состояния.

Compose поставляется с функциями для создания State<T> из распространенных наблюдаемых типов, используемых в приложениях Android. Прежде чем использовать эти интеграции, добавьте соответствующие артефакты, как описано ниже:

  • Flow : collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() собирает значения из Flow с учетом жизненного цикла, что позволяет вашему приложению экономить ресурсы приложения. Он представляет собой последнее значение, отправленное из State Compose. Используйте этот API как рекомендуемый способ сбора потоков в приложениях Android.

    В файле build.gradle требуется следующая зависимость (это должна быть версия 2.6.0-beta01 или новее):

Котлин

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

классный

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow : collectAsState()

    collectAsState похож на collectAsStateWithLifecycle , поскольку он также собирает значения из Flow и преобразует их в Compose State .

    Используйте collectAsState для кода, не зависящего от платформы, вместо collectAsStateWithLifecycle , который доступен только для Android.

    Дополнительные зависимости для collectAsState не требуются, поскольку они доступны в compose-runtime .

  • LiveData : observeAsState()

    observeAsState() начинает наблюдать за этими LiveData и представляет их значения через State .

    В файле build.gradle требуется следующая зависимость :

Котлин

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}

классный

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}

Котлин

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}

классный

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}

Котлин

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}

классный

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}

С состоянием и без гражданства

Компонуемый объект, использующий функцию remember сохранить объект», создает внутреннее состояние, делая компонуемый объект сохраняющим состояние . HelloContent — это пример составного объекта с сохранением состояния, поскольку он внутренне сохраняет и изменяет состояние своего name . Это может быть полезно в ситуациях, когда вызывающему объекту не нужно контролировать состояние и он может использовать его без необходимости управлять состоянием самостоятельно. Однако составные элементы с внутренним состоянием, как правило, менее пригодны для повторного использования и их сложнее тестировать.

Составной объект без сохранения состояния — это составной объект, который не содержит никакого состояния. Простой способ добиться отсутствия гражданства — использовать подъем состояния .

При разработке многоразовых составных элементов часто требуется предоставить как версию одного и того же составного объекта как с сохранением состояния, так и без сохранения состояния. Версия с сохранением состояния удобна для вызывающих абонентов, которым не важно состояние, а версия без сохранения состояния необходима для вызывающих абонентов, которым необходимо контролировать или повышать состояние.

Государственный подъем

Поднятие состояния в Compose — это шаблон перемещения состояния к вызывающей стороне составного объекта, чтобы сделать составной объект без состояния. Общий шаблон подъема состояния в 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 что в качестве параметра key1 принимается avatarRes , который представляет собой выбранное фоновое изображение. Если 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 использует реализацию равенства класса, чтобы определить, изменился ли ключ, и сделать сохраненное значение недействительным.

Сохранение состояния с ключами, не подлежащими рекомпозиции

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, обратитесь к следующим дополнительным ресурсам.

Образцы

Кодлабы

Видео

Блоги

{% дословно %} {% дословно %} {% дословно %} {% дословно %}