Урок по треку Android 8 мин

Compose: композиция и рекомпозиция за 8 минут

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

Jetpack Compose это декларативный UI: вы описываете, как экран выглядит для данного состояния, а фреймворк сам обновляет то, что изменилось. Главная тема собесов по Compose это рекомпозиция: когда она происходит, что пропускается и почему экран «тормозит».

Composable-функции

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(text = "Привет, $name", modifier = modifier.padding(16.dp))
}

Composable-функция ничего не возвращает, она «излучает» UI в дерево композиции. Аннотацию @Composable обрабатывает плагин компилятора Compose: он добавляет скрытый параметр Composer и группы, по которым рантайм отслеживает дерево. Поэтому composable можно вызывать только из другой composable.

Три фазы кадра:

  1. Composition: выполняются composable-функции, строится или обновляется дерево.
  2. Layout: измерение и расстановка элементов.
  3. Drawing: отрисовка.

Чтение состояния в каждой фазе вызывает повтор только этой и последующих фаз. Если состояние читается в лямбде Modifier.offset { } или drawBehind { }, при его изменении композиция не повторяется, только layout или отрисовка. Это главный приём оптимизации анимаций.

Рекомпозиция

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

  • Рекомпозиция может пропустить функцию, если её параметры не изменились (skipping).
  • Порядок и число вызовов не гарантированы, рекомпозиция может быть отменена и перезапущена. Поэтому composable-функции должны быть без побочных эффектов: никаких сетевых вызовов, записи в переменные снаружи, логирования аналитики прямо в теле.
  • Функции вызываются часто, вплоть до каждого кадра анимации. Тяжёлые вычисления в теле нужно кешировать (remember).

Стабильность и пропуск

Compose пропускает рекомпозицию функции, если все параметры стабильны и равны прошлым. Стабильный тип: изменения в нём видны Compose, а equals надёжен. Стабильны примитивы, String, функциональные типы (лямбды), классы только с val стабильных типов, State<T>, enum.

Нестабильны: классы с var-полями, интерфейсы коллекций List, Map, Set (под ними может быть изменяемая реализация), классы из модулей без компилятора Compose.

С Strong Skipping Mode (включён по умолчанию с Kotlin 2.0.20 и компилятора Compose в составе Kotlin) правила мягче: функции с нестабильными параметрами тоже пропускаются, если параметры равны по ссылке (===), а лямбды автоматически запоминаются. Но стабильность по-прежнему важна: новый экземпляр List на каждой рекомпозиции родителя не даст пропустить ребёнка.

Как сделать стабильным: неизменяемые data class с val, kotlinx.collections.immutable (ImmutableList), аннотации @Immutable и @Stable (обещание компилятору, проверять их никто не будет), файл конфигурации стабильности для классов из чужих модулей. Диагностика: отчёты компилятора Compose (composition metrics) и Layout Inspector со счётчиками рекомпозиций.

Modifier

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

Modifier.padding(16.dp).background(Color.Red)   // отступ снаружи фона
Modifier.background(Color.Red).padding(16.dp)   // фон под отступом
Modifier.clickable { }.padding(8.dp)            // кликабельна и область отступа

Хорошая практика: каждая переиспользуемая composable принимает modifier: Modifier = Modifier первым необязательным параметром и применяет его к корневому элементу.

Списки

Column с forEach создаёт все элементы сразу. Для длинных списков LazyColumn/LazyRow: компонуются только видимые элементы.

LazyColumn {
    items(users, key = { it.id }) { user ->
        UserRow(user)
    }
}

key обязателен для изменяемых списков: без него состояние элементов привязано к позиции, и при вставке в начало «съедет», а анимации animateItem() не работают. contentType помогает переиспользовать композиции разных типов строк.

Compose и View

ComposeView вставляет Compose в View-иерархию, AndroidView наоборот встраивает View в Compose. Это позволяет мигрировать экран за экраном.

Частые ошибки

  • Побочные эффекты прямо в теле composable (запрос, запись в переменную, viewModel.load() без эффекта).
  • Тяжёлые вычисления в теле без remember.
  • Передача нестабильных типов (List, классы с var) и создание новых объектов на каждой рекомпозиции.
  • Чтение быстро меняющегося состояния (скролл, анимация) в фазе композиции вместо layout/draw.
  • LazyColumn без key.
  • Неправильный порядок модификаторов.

Что спросят на собесе

  • Что такое рекомпозиция и когда она происходит?
  • Что такое стабильность типа и как она влияет на пропуск?
  • Что изменил Strong Skipping Mode?
  • Почему в composable нельзя делать побочные эффекты?
  • Какие фазы у кадра Compose и как это использовать для оптимизации?
  • Зачем key в LazyColumn?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.