Как происходит пересчет UI в Jetpack Compose
Что проверяют
Понимаете ли вы, что Compose не «перерисовывает экран», а заново вызывает только те composable-функции, которые читали изменившееся состояние, и умеете ли этим управлять. Следом почти всегда спрашивают про стабильность параметров.
Как отвечать
Начните с трёх фаз кадра: Composition (что показать), Layout (измерение и расположение), Drawing (отрисовка). Изменение состояния может затронуть любую из них, и чем позже фаза, тем дешевле.
Дальше механизм:
- snapshot-состояние: когда composable читает значение из
mutableStateOf, его область рекомпозиции подписывается на это состояние, а запись помечает её недействительной; - рекомпозиция: на следующем кадре Compose заново вызывает только недействительные области, а не всё дерево;
- пропуск (skipping): функция не вызывается повторно, если её параметры не изменились. Стабильные типы сравниваются через
equals. С Kotlin 2.0.20 по умолчанию включён strong skipping mode: нестабильные параметры тоже не мешают пропуску, но сравниваются по ссылке; remember: хранит значение между рекомпозициями;derivedStateOf: превращает часто меняющееся состояние в редко меняющееся, например флаг «показать кнопку наверх» из позиции скролла;- отложенное чтение:
Modifier.offset { }илиgraphicsLayer { }с лямбдой читают состояние в фазе layout или draw, и композиция не повторяется вовсе.
В LazyColumn передавайте key в items, иначе при вставке элемента его состояние съедет к соседу. Гипотезы проверяйте счётчиками рекомпозиций в Layout Inspector и отчётами компилятора Compose о стабильности.
Где спотыкаются
Пишут побочные эффекты прямо в теле composable: запрос в сеть, изменение состояния. Рекомпозиция может случиться сколько угодно раз и быть отменена, для эффектов есть LaunchedEffect и SideEffect.
Создают в теле функции новый список через listOf или map и передают его вниз. Сравнение по ссылке каждый раз видит новый объект, и дочерние функции перестают пропускаться.
Читают быстро меняющееся значение вроде позиции скролла высоко в дереве. Рекомпозиция расползается на весь экран, хотя хватило бы derivedStateOf или чтения в лямбде модификатора.