Введение: состояние, которое меняется слишком часто
«Пользователь скроллит список, и каждое изменение позиции вызывает рекомпозицию. А показывать нужно только одну кнопку — "наверх", когда список прокручен». Задача, на которой я ловлю большинство кандидатов: одни не знают, что делать, другие — достают derivedStateOf и snapshotFlow налево и направо, не понимая, зачем они вообще нужны.
Оба API решают по сути одну проблему: согласовать частоту изменения состояния с частотой, в которой оно действительно нужно UI. Но работают они с разными сторонами этого мира: derivedStateOf — это про состояние Compose, а snapshotFlow — мост из Compose в корутины и Flow.
Разберём оба: как работают, когда нужны, когда категорически нет — и что на эту тему спрашивают на собеседованиях.
derivedStateOf: когда входные данные меняются чаще, чем нужно
Официальная рекомендация звучит так: derivedStateOf нужен, когда входные данные композиции меняются чаще, чем требуется её перекомпоновка. Он создаёт новое наблюдаемое состояние, которое обновляется только тогда, когда изменился сам результат — по поведению это похоже на оператор distinctUntilChanged() из мира Flow.
Канонический пример из документации по побочным эффектам Compose — кнопка «наверх», которая появляется после прокрутки списка:
@Composable
fun MessageList(messages: List<Message>) {
val listState = rememberLazyListState()
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
Box {
LazyColumn(state = listState) {
// список сообщений
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
}
}Разберём, что здесь происходит. firstVisibleItemIndex при прокрутке принимает значения 0, 1, 2, 3… — каждое изменение этого значения теоретически требует рекомпозиции. Но UI важен только один факт: индекс больше нуля или нет. derivedStateOf отсекает промежуточные значения: 1, 2, 3 — это всё ещё «true», значит, перекомпоновываться не нужно.
Когда использовать
derivedStateOf — для ситуаций, где вход меняется часто, а UI реагирует редко: позиция прокрутки, прогресс анимации, пороги значений. Если вход и выход меняются с одинаковой частотой — API не нужно.
Когда derivedStateOf не нужен
Теперь обратная сторона. derivedStateOf — ресурсоёмкий механизм, и документация прямо предупреждает: использовать его стоит только для предотвращения лишних рекомпозиций, когда результат не изменился.
Самая частая ошибка — оборачивать в derivedStateOf любое «вычисляемое» значение. Классический антипример из официальной документации:
// Так делать НЕ нужно — антипример из документации
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // плохо
val fullNameCorrect = "$firstName $lastName" // правильноПочему «правильно» — обычная строка? Потому что fullName должен обновляться ровно с той же частотой, что и firstName с lastName: каждое изменение фамилии меняет полное имя. Лишней рекомпозиции не возникает — значит, derivedStateOf добавляет только накладные расходы.
Проверка на собеседовании: кандидат говорит «я вычисляю производное состояние, поэтому взял derivedStateOf» — я прошу объяснить, меняется ли результат реже, чем входные данные. Если нет — derivedStateOf здесь не нужен. Этот вопрос отсеивает заученные ответы мгновенно.
snapshotFlow: мост из состояния Compose в Flow
snapshotFlow решает смежную задачу: он превращает State<T> в холодный Flow. Блок snapshotFlow выполняется при старте сбора, читает состояния и выдаёт результат; когда одно из прочитанных состояний меняется, поток выдаёт новое значение — но только если оно отличается от предыдущего, что тоже напоминает distinctUntilChanged().
Зачем это нужно? Затем, что после превращения состояния в Flow к нему применяются все операторы корутин: map, filter, collect — и эффекты вроде аналитики становятся тривиальными. Пример из документации:
@Composable
fun ScrollAnalytics(listState: LazyListState) {
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
}Обратите внимание на пару деталей. snapshotFlow стартует при сборе — поэтому его оборачивают в LaunchedEffect, который и запускает сбор. А distinctUntilChanged() в цепочке не лишний: snapshotFlow отсекает повторяющиеся значения самого состояния, а после map одинаковые результаты появляются снова — и вот их уже отсекает оператор.
Когда выбирать
derivedStateOf — если производное значение нужно внутри Compose (видимость, текст, цвет). snapshotFlow — если состояние нужно передать в мир корутин: аналитика, debounce, сохранение при уходе с экрана, объединение с другими потоками через combine.
Разница хорошо видна на одном и том же примере прокрутки: для кнопки «наверх» нужен derivedStateOf (состояние UI), для аналитики — snapshotFlow (поток событий). Кандидат, который объясняет это различие своими словами, — проходит.
И ещё один сценарий, где snapshotFlow незаменим, — объединение с другими потоками. Состояние Compose само по себе изолировано, но через snapshotFlow оно становится обычным Flow и может участвовать в combine с потоками из репозитория или в операторе debounce для поиска с задержкой. Так связываются два мира, которые иначе пришлось бы синхронизировать вручную.
Частая путаница на собеседовании: «snapshotFlow — горячий поток, потому что состояние уже есть». Нет: поток холодный и стартует при сборе. Именно поэтому без LaunchedEffect (или аналогичного места сбора) блок snapshotFlow не выполнится ни разу.
Ошибки на собеседованиях
Как и с любыми API состояния, здесь ошибки делятся на «не знаю» и «злоупотребляю». Типичный набор с моих собеседований.
derivedStateOfдля любого вычисления. «Сложил два состояния — завернул в derivedStateOf». Лишний ресурсоёмкий механизм там, где хватило обычной строки или выражения.derivedStateOfвнеremember. Безrememberсостояние пересоздаётся при каждой рекомпозиции — и вся экономия пропадает. Забыли обёртку — получили противоположный эффект.snapshotFlowбезLaunchedEffect. Поток холодный: без сбора ничего не происходит. Собирать нужно в эффекте, а не «где-то рядом».- Непонимание, что snapshotFlow отсекает дубликаты. Эмиссия происходит только при изменении значения — как у
distinctUntilChanged(). После трансформаций операторами это свойство теряется, и фильтр нужен снова. - «snapshotFlow и derivedStateOf — одно и то же». Нет: один создаёт состояние для Compose, второй — поток для корутин. Разные миры, одна идея про частоту изменений.
Главный вопрос
«Чем derivedStateOf отличается от snapshotFlow?» — если кандидат отвечает «derivedStateOf — состояние для UI, snapshotFlow — мост в Flow», тема закрыта. Всё остальное — детали.
Практический совет по подготовке: не учите эти API «впрок». Возьмите реальный экран — список с прокруткой, поле поиска, прогресс — и найдите место, где входные данные меняются чаще, чем нужно. Если нашли такое место — посмотрите, что из двух API уместнее. Если не нашли — значит, они вам сейчас и не нужны, и это тоже правильный ответ на собеседовании.
Пара слов обо мне: я Рустем Бикбулатов, senior Android-разработчик, провожу технические собеседования и менторю разработчиков, в том числе как ментор Яндекс Практикума. За плечами более 100 учеников и разборов — и состояние в Compose одна из тем, где кандидаты с опытом 1–2 года чаще всего плавают. Есть диагностика: разбор вашего уровня и план закрытия пробелов.
Хотите понять, какие пробелы мешают вам расти?
Бесплатная диагностика: разберу ваши знания Compose, Kotlin и корутин и составлю план роста под ваш уровень.
Итоги
Соберём тему в три тезиса:
derivedStateOfнужен, когда входные данные меняются чаще, чем требуется перекомпоновка; результат обновляется только при реальном изменении — какdistinctUntilChanged().snapshotFlowпревращает состояние Compose в холодный Flow — дальше работают операторы корутин, отmapдоdebounce.- Оба API не для всего подряд: если вход и выход меняются одинаково часто или поток не нужен — они только добавляют сложность.
Главное, что нужно запомнить
Вопрос «что выбрать» решается за секунду: производное значение нужно UI — derivedStateOf; нужно корутинам — snapshotFlow; не нужно ни то ни другое — не используйте ничего.
Если чувствуете, что знаний по кускам много, а уверенно ответить на вопросы про Compose всё равно не можете — не нужно зубрить дальше. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.