0Pricing
Android Academy · Урок

Управление рекомпозицией

Стабильные параметры и меньше рекомпозиций

«Управление рекомпозицией» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Измерение производительности
  2. Управление рекомпозицией
  3. Утечки памяти и исправления
  4. Профили запуска и Baseline Profiles
← Назад к Android Academy