Управление рекомпозицией
Стабильные параметры и меньше рекомпозиций
«Управление рекомпозицией» — бесплатный урок Android Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 4 уроков всего.
Рекомпозиция: регулятор производительности Compose
В Jetpack Compose пользовательский интерфейс описывается функциями. Когда состояние изменяется, Compose выполняет рекомпозицию: повторно запускает компонуемые функции, которые читают это состояние, чтобы обновить экран.
Рекомпозиция — нормальный и недорогой процесс, если правильно ограничить её область. Но если она происходит слишком часто или охватывает слишком большую часть дерева, она становится причиной № 1 подёргиваний в Compose. В этом уроке вы научитесь делать рекомпозицию небольшой и редкой.
Просмотр количества рекомпозиций
Прежде чем исправлять проблему, измерьте её. Инспектор макетов в Android Studio показывает количество рекомпозиций каждой компонуемой функции в реальном времени. Также можно использовать метрики компилятора Compose или простой счётчик для отладки.
Простой приём: SideEffect увеличивает счётчик при каждой рекомпозиции компонуемой функции, чтобы во время разработки вы могли записывать неожиданные случаи в журнал.
@Composable
fun RecompositionCounter(tag: String) {
val count = remember { mutableStateOf(0) }
SideEffect { count.value++ }
Log.d("Recompose", "$tag recomposed ${count.value} times")
}
// Drop RecompositionCounter("PriceLabel") inside a composable
// to watch how often it re-runs while you interact.Читайте состояние как можно позже
Compose повторно компилирует только те компонуемые функции, которые читают значение состояния. Если состояние читает родитель, рекомпозируется весь родитель; если его читает только небольшой дочерний элемент, рекомпозируется лишь этот элемент.
Поэтому переносите чтение состояния ниже по дереву. В неудачном варианте весь Column рекомпозируется при каждом изменении, а в удачном это ограничено подписью.
// BAD: Column reads `seconds`, so everything recomposes each second
@Composable
fun TimerBad(seconds: Int) {
Column {
ExpensiveHeader()
Text("Elapsed: $seconds")
}
}
// GOOD: only the Text reads the value via a lambda
@Composable
fun TimerGood(seconds: () -> Int) {
Column {
ExpensiveHeader()
Text("Elapsed: ${seconds()}")
}
}Откладывайте чтение с помощью лямбд
Мощный приём: вместо часто изменяющегося значения передавайте лямбду, которая его возвращает. Рекомпозируется только та компонуемая функция, которая в итоге вызывает лямбду.
Именно поэтому Modifier.offset { ... } и graphicsLayer { ... } принимают лямбды: значения прокрутки и анимации меняются в каждом кадре, а лямбда полностью исключает рекомпозицию из фазы компоновки.
// Passing the value: parent recomposes every frame of scroll
Box(Modifier.offset(y = scrollOffset.dp))
// Passing a lambda: skips recomposition, updates in the layout phase
Box(Modifier.offset { IntOffset(x = 0, y = scrollOffset.roundToInt()) })
// Same idea for alpha/scale during animation:
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.graphicsLayer { alpha = animatedAlpha() }
)Стабильность: почему Compose пропускает рекомпозицию
Compose может пропустить рекомпозицию компонуемой функции, если все её параметры стабильны и не изменились. Тип считается стабильным, если Compose может полагаться на то, что equals отражает реальные изменения, а его открытые поля не изменяются незаметно.
- Стабильные: примитивы,
String, неизменяемые классы данных,State. - Нестабильные: интерфейсы
List/Map, классы с полямиvar, типы из модулей без компилятора.
Нестабильный параметр заставляет выполнять рекомпозицию, даже если ничего не изменилось.
// UNSTABLE: List is an interface; Compose cannot assume immutability,
// so UserList recomposes even if the contents are identical.
@Composable
fun UserList(users: List<User>) { /* ... */ }
data class User(val id: Long, val name: String) // stable: all valsСделайте параметры стабильными
Два распространённых способа исправить нестабильные параметры:
- Используйте неизменяемые коллекции из
kotlinx.collections.immutable(например,ImmutableList), которые Compose считает стабильными. - Пометьте управляемый вами класс аннотацией
@Immutableили@Stable, гарантируя Compose, что он не будет изменяться.
Теперь Compose может безопасно пропустить рекомпозицию, если ему снова передан тот же экземпляр.
import kotlinx.collections.immutable.ImmutableList
import androidx.compose.runtime.Immutable
@Immutable
data class UiState(
val title: String,
val users: ImmutableList<User>
)
// Stable parameter -> Compose can skip this when state is unchanged
@Composable
fun UserList(users: ImmutableList<User>) { /* ... */ }remember: не пересчитывайте всё в каждом кадре
Компонуемые функции могут выполняться много раз. Любое нетривиальное вычисление, выполненное непосредственно в теле, запускается при каждой рекомпозиции. Оберните его в remember, чтобы пересчитывать значение только при изменении его ключей.
Для значений, полученных из других состояний, предпочтительно использовать derivedStateOf: оно выдаёт новое значение только при фактическом изменении вычисленного результата.
// Recomputes the sorted list only when `items` changes
val sorted = remember(items) { items.sortedBy { it.name } }
// derivedStateOf: only triggers readers when the BOOLEAN flips,
// not on every scroll pixel
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (showButton) ScrollToTopButton()Стабильные ключи в ленивых списках
В LazyColumn/LazyRow задавайте каждому элементу стабильный ключ. Без ключей вставка или перестановка элементов заставляет Compose повторно компилировать и измерять элементы, которые на самом деле не изменились, поскольку он отслеживает их по позиции.
Съ стабильным ключом Compose может сопоставлять элементы при обновлениях и пропускать неизменившиеся.
LazyColumn {
items(
items = users,
key = { user -> user.id } // stable identity
) { user ->
UserRow(user)
}
}
// Now adding a user at the top reuses existing rows
// instead of recomposing the whole list.Поднимайте состояние, передавайте события вверх
Нестабильные параметры-лямбды также могут нарушить пропуск рекомпозиции, если при каждой рекомпозиции создаётся новый экземпляр лямбды. Стабилизируйте обратные вызовы, запоминая их или ссылаясь на стабильные функции.
В сочетании с поднятием состояния (состояние хранится у вызывающего кода, события передаются вверх) это позволяет конечным компонуемым функциям оставаться дешёвыми и пригодными для пропуска.
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
TextField(value = query, onValueChange = onQueryChange)
}
// In the caller, a remembered lambda keeps the reference stable:
val onChange = remember { { newValue: String -> viewModel.setQuery(newValue) } }
SearchBar(query = query, onQueryChange = onChange)Не читайте состояние прокрутки и анимации слишком высоко
Классическая ошибка — читать быстро изменяющееся состояние (смещение прокрутки, ход анимации) в компонуемой функции высокого уровня. Это заставляет всё поддерево рекомпозироваться в каждом кадре.
Оставляйте такие чтения внутри лямбд Modifier (offset {}, graphicsLayer {}, drawBehind {}), чтобы работа выполнялась на этапе компоновки или отрисовки, а не во время рекомпозиции. Именно это отличает плавную прокрутку со скоростью 60 кадров/с от прерывистой.
// Animated color used only for drawing -> stay in the draw phase
Box(
Modifier.drawBehind {
drawRect(color = animatedColor()) // lambda read, no recomposition
}
)Список проверки рекомпозиции
Если экран начинает подёргиваться во время взаимодействия, пройдитесь по этому списку:
- Параметры стабильны (неизменяемые данные,
ImmutableList)? - Вы читаете быстро изменяющееся состояние как можно ниже, в идеале в лямбдах модификаторов?
- Есть ли у ленивых списков стабильные ключи?
- Находятся ли ресурсоёмкие вычисления за
remember/derivedStateOf? - Подтвердил ли инспектор макетов, что количество действительно уменьшилось?
Каждый отмеченный пункт устраняет лишнюю работу из каждого кадра.
Быстрая проверка
Вы передаёте List<User> компонуемой функции и замечаете, что она рекомпозируется, даже когда данные не изменились. Какое исправление будет самым прямым?
Повторение: рекомпозиция становится меньше и реже
Вы научились обуздывать главную проблему производительности Compose — рекомпозицию:
- Измеряйте количество рекомпозиций с помощью инспектора макетов или счётчика для отладки.
- Читайте состояние как можно ниже; откладывайте чтение с помощью лямбд.
- Делайте параметры стабильными с помощью неизменяемых данных и
@Immutable/ImmutableList. - Используйте
rememberиderivedStateOf, чтобы не выполнять вычисления заново в каждом кадре. - Задавайте ленивым элементам стабильные ключи; оставляйте чтение быстро изменяющегося состояния в лямбдах модификаторов.
Далее мы перейдём от CPU и рекомпозиции к памяти: поиску и устранению утечек.
Часто задаваемые вопросы
Урок «Управление рекомпозицией» бесплатный?
Да — полный текст урока «Управление рекомпозицией» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Android Academy, подпишись на CoddyKit PRO. Курс Android Academy содержит 4 уроков всего.
Чему я научусь в уроке «Управление рекомпозицией»?
Стабильные параметры и меньше рекомпозиций Ты практикуешь Android Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Android Academy?
Предыдущий опыт не требуется. Android Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Управление рекомпозицией»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Android Academy?
Да. Каждый урок Android Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Измерение производительности
- Управление рекомпозицией
- Утечки памяти и исправления
- Профили запуска и Baseline Profiles