Производительность в Jetpack Compose: стабильность и неизменяемость

При изменении состояния Jetpack Compose может автоматически пропускать composable-функции, параметры которых остались без изменений. Это обеспечивает эффективный рендеринг и повышает производительность. Но в некоторых ситуациях такого не наблюдается, и Jetpack Compose вынужден проводить рекомпозицию composable-функций.


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

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Stability_ImmutabilityTheme {
val (isChecked, onChecked) = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
) {
Checkbox(
checked = isChecked,
onCheckedChange = onChecked
)
ContactList(
contactListState = ContactListState(
names = listOf("name1", "name2", "name3", "name4"),
)
)
}
}
}
}
}


@Composable
fun ContactList(
contactListState: ContactListState
) {
if (contactListState.isLoading) {
CircularProgressIndicator()
} else {
Text(text = contactListState.names.toString())
}
}

data class ContactListState(
val names: List<String>,
val isLoading: Boolean = false,
)

В этом примере у нас есть чекбокс и composable-функция Contact List, которая отображает список контактов.

С помощью инструмента Layout Inspector мы можем наблюдать, какие элементы composables перекомпонуются при изменении состояния. При этом перекомпонованные элементы подсвечиваются синим цветом. В то же время Layout Inspector показывает, сколько composable-функций было перекомпоновано и сколько было пропущено.

Инструмент Layout Inspector можно открыть в разделе Tools/Layout Inspector в Android Studio.

В нашем примере мы изменяем только состояние чекбокса, однако composable-функция Contact List была перекомпонована четыре раза, хотя ни разу не менялись параметры Contact List.


Перезапускаемые и пропускаемые функции в Jetpack Compose

Прежде чем рассказать о перезапускаемых и пропускаемых функциях, дадим определение рекомпозиции.

Рекомпозиция (перекомпоновка)  —  это процесс повторного вызова composable-функций при изменении входных данных. Это происходит, когда изменяются входные данные функции. Когда Compose выполняет рекомпозицию на основе новых входных данных, он вызывает только те функции и лямбды, которые могли измениться, а остальные пропускает (документация Android).

Компилятор Compose помечает функции как перезапускаемые (restartable) и пропускаемые (skippable), чтобы определить, является ли она пропускаемой или перезапускаемой.

Перезапускаемая функция (restartable)

Пометка функции как restartable означает, что она может быть вызвана снова, если произойдут какие-либо изменения ее входных данных.

Пропускаемая функция (skippable)

Если функция помечена как пропускаемая (skippable), можно пропустить ее вызов, если параметры не изменились с момента последнего вызова.

Как узнать, что функция помечена как перезапускаемая и/или пропускаемая?

Включите отчет о метриках компилятора Compose

subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_compiler",
)
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_compiler",
)
}
}

Запишите приведенный выше скрипт в файл build.gradle.kts (Project).

Чтобы получить результат отчета, выполните следующую команду:

./gradlew assembleRelease -PcomposeCompilerReports=true

Эта задача сгенерирует четыре файла в каталоге build каждого модуля под файлом compose_compiler.

  1. app_release-classes.txt: отчет о стабильности классов.
  2. app_release-composables.txt: отчет о перезапускаемых и пропускаемых composable-функциях.
  3. app_release-composables.csv: версия вышеуказанного текстового файла в формате csv.
  4. app_release-module.json: содержит общую статистику.

После выполнения задачи откройте файл app_release-composables.txt  —  вы увидите все composable-функции. Каждая из них будет помечена либо как перезапускаемая (restartable), либо как пропускаемая (skippable).

restartable scheme("[androidx.compose.ui.UiComposable]") fun ContactList(
unstable contactListState: ContactListState
)

Composable-функция ContactList помечена как restartable, а не как skippable. Почему?

Параметр ContactListState помечен как нестабильный (unstable). Что это означает с точки зрения стабильности и как сделать его стабильным?

Стабильность

Стабильность параметров composable-функции имеет решающее значение для определения того, нужно ли проводить ее повторную оценку при рекомпозиции в компиляторе Compose.

Стабильные параметры

  • Если параметры composable-функции стабильны и не изменились во время рекомпозиции, Compose пропускает ее.
  • Так, стабильными параметрами являются типы String, Int, Float и ImmutableList.

Нестабильные параметры

  • Если composable-функция имеет нестабильные параметры, Compose всегда проводит рекомпозицию, даже если параметры не изменились.
  • Например, типы List, Map и Set являются нестабильными параметрами. Также нестабильными являются объявления var в Data class.
val numbers: List<Int> = mutableListOf(1, 2, 3)

Поскольку стандартные классы Collection определены в Kotlin как интерфейсы, их базовая реализация может оказаться нестабильной. Компилятор Compose не уверен в неизменности этого класса, поскольку видит только объявленный тип, и помечает его как нестабильный.


Посмотрим на класс данных ContactListState. Чтобы увидеть отчет, откроем файл app_release-classes.

unstable class ContactListState {
unstable val names: List<String>
stable val isLoading: Boolean
<runtime stability> = Unstable
}

Как мы уже говорили, параметр List нестабилен. Если класс данных имеет нестабильные параметры, он также будет нестабильным.


Стабильные и неизменяемые аннотации

Неизменяемые аннотации. Как следует из названия, они содержат неизменяемые данные. Поскольку эти данные никогда не меняются, Compose может рассматривать их как стабильные.

@Immutable
data class ContactListState(
val names: List<String>,
val isLoading: Boolean = false,
)

Стабильные аннотации. В них хранятся данные, которые можно изменять, но при изменении необходимо уведомление на этапе Composition.

@Stable
data class ContactListState(
val names: List<String>,
val isLoading: Boolean = false,
)

Мы можем использовать любую из этих аннотаций. После внесения изменений повторно запустим метрики и посмотрим на результаты.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ContactList(
stable contactListState: ContactListState
)

Composable-функция ContactList является перезапускаемой и пропускаемой. А параметры ContactList стабильны.

При изменении состояния только чекбокс подвергается рекомпозиции. Поскольку ни один из параметров ContactList не изменился и все они являются стабильными, Compose может спокойно пропустить эту функцию.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Tolga Pirim: Performance In Jetpack Compose — Stability & Immutability

Предыдущая статьяПрогнозирование настроений на фондовом рынке с помощью OpenAI и Python
Следующая статьяGo — единственный выбор для бэкенд-разработчика?