State и Jetpack Compose

Состояние в приложении — это любое значение, которое может изменяться со временем. Это очень широкое определение, охватывающее всё, от базы данных 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 с учетом жизненного цикла, что позволяет вашему приложению экономить ресурсы. Он представляет собой последнее значение, полученное из Compose State . Используйте этот API как рекомендуемый способ сбора данных из Flow в приложениях Android.

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

Котлин

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

Классный

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.10.0"
}
  • 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.11.4")
}

Классный

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

Котлин

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

Классный

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

Котлин

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

Классный

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

С сохранением состояния против без сохранения состояния

Композируемый объект, использующий 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 . Следуя однонаправленному потоку данных, вы можете отделить компоненты, отображающие состояние в пользовательском интерфейсе, от частей приложения, которые хранят и изменяют состояние.

  1. Состояние следует поднять как минимум до самого нижнего общего родительского элемента среди всех компонуемых объектов, использующих это состояние (чтение).
  2. Государство должно быть поднято как минимум на самый высокий уровень, на котором оно может быть изменено (написано).
  3. Если два государства изменяются в ответ на одни и те же события, их следует поднимать одновременно.

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

Для получения более подробной информации посетите страницу «Где поднять оборудование в штате» .

Восстановление состояния в 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 %} {% endverbatim %} {% verbatim %} {% endverbatim %}