Введение: Compose — теперь обязательная тема
«Расскажите, как устроен Jetpack Compose» — открытый вопрос, с которого начинается проверка UI-части почти на каждом собеседовании. И он же роняет кандидатов, которые писали на Compose, но не строили ментальную модель.
Пять лет назад можно было ответить «работаю с XML, RecyclerView и адаптерами» — и этого хватало. Сегодня Compose — основной способ создания UI в Android, новые проекты на нём, и интервьюеры копают глубже, чем «какой модификатор для отступов». Проверяют декларативную модель, состояние, жизненный цикл composable-функций и сайд-эффекты — всё то, что отличает «написал экран» от «понимаю, как оно работает».
В этой статье — полный разбор темы для собеседования: от ментальной модели до типичных ошибок кандидатов. Если готовитесь — читайте, а потом попробуйте ответить на вопросы из статьи вслух.
Декларативная модель: UI как функция состояния
Главное, что нужно объяснить на собеседовании: в Compose UI — это функция состояния, а не набор инструкций. В императивном подходе (XML) ты описываешь, как построить экран: создать View, найти его по id, обновить вручную при изменении данных. В декларативном — ты описываешь, каким экран должен быть при текущем состоянии, и фреймворк сам приводит UI к этому описанию.
@Composable
fun ProfileScreen(user: User) {
// поменялся user — поменялось описание UI, Compose перестроит экран
Column {
Avatar(user.avatarUrl)
Text(user.name)
Bio(user.bio)
}
}Когда состояние меняется, Compose пересоздаёт затронутые функции — это рекомпозиция. Подробно про неё я писал отдельно: Recomposition в Compose: как работает. Здесь зафиксируем главное: Compose пересоздаёт не весь экран, а только функции, входные данные которых могли измениться, и делает это эффективно.
Дальше — два вопроса, которые интервьюер задаст почти наверняка:
- Как Compose узнаёт, что состояние изменилось? Через чтение: функция, которая прочитала
State, подписывается на него. Изменилось значение — функция пересоздаётся. - Что такое однонаправленный поток данных? Состояние течёт вниз (от родителя к детям), события — вверх (от детей к родителю через колбэки). Никакого «состояние живёт само по себе» в произвольном месте иерархии.
Короткая формула
UI = f(state). Функция описала зависимость от состояния один раз, дальше Compose сам следит за изменениями. Читаешь State в composable — подписываешься на изменения. Передаёшь события через лямбды — поток данных остаётся однонаправленным.
Состояние: mutableStateOf, remember и подъём
В Compose состояние создаётся через mutableStateOf. Само по себе оно не переживает рекомпозицию — если функцию пересоздали, обычная переменная создастся заново. Чтобы состояние пережило пересоздание, его оборачивают в remember:
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column {
Text("Счёт: $count")
Button(onClick = { count++ }) {
Text("+1")
}
}
}Ключевые понятия, которые нужно уверенно различать:
remember— сохраняет значение между рекомпозициями, но не переживает выход из композиции (например, удаление элемента из списка).rememberSaveable— переживает не только рекомпозицию, но и воссоздание Activity (поворот экрана): значение уходит в Bundle.remember(key)— сбрасывает значение, когда ключ меняется: например, при сменеuserIdнужно пересоздать состояние экрана пользователя.derivedStateOf— для состояний, которые вычисляются из других: например, «можно ли нажать кнопку» на основе списка введённых полей.
Второй блок вопросов — про подъём состояния (state hoisting). Правило простое: состояние должно жить на самом низком уровне иерархии, где оно нужно, но подниматься выше, как только нужно нескольким функциям или когда его нужно сбрасывать.
@Composable
fun CounterDemo() {
var count by remember { mutableStateOf(0) }
// состояние поднято наверх, дети — статeless-функции
Counter(count = count, onIncrement = { count++ })
}
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Column {
Text("Счёт: $count")
Button(onClick = onIncrement) { Text("+1") }
}
}Статeless-компоненты легче тестировать, переиспользовать и понимать. На собеседовании фраза «я выношу состояние наверх, когда оно нужно нескольким компонентам, и передаю изменения через колбэки» закрывает половину вопросов про архитектуру UI.
Жизненный цикл composable и сайд-эффекты
У composable-функции три фазы жизненного цикла: вход в композицию (composition), рекомпозиция (сколько угодно раз) и выход из композиции (disposition). На собеседовании важно показать, что ты понимаешь последствия: обычный код в теле функции выполняется при каждом пересоздании, а код с привязкой к фазе нужно выносить в сайд-эффекты.
Сайд-эффекты — это операции, которые должны происходить в связи с жизненным циклом: подписки, запуск корутин, показ диалогов. Запускать их прямо в теле функции нельзя — рекомпозиция может быть пропущена или выполнена многократно. Для этого есть специальные API из документации:
LaunchedEffect(key)— запускает корутину при входе в композицию, отменяет при выходе. Классика: загрузка данных по ключу.DisposableEffect(key)— для работы с ресурсами: подписка на слушатель вonDisposeотменяется.rememberCoroutineScope— скоуп, привязанный к композиции, для запуска корутин по событиям UI (например, по клику).SideEffect— выполняется после каждой успешной рекомпозиции; редкий гость на собеседованиях, но полезно упомянуть.
@Composable
fun UserItem(userId: String) {
// корутина запускается один раз при входе в композицию
// и отменяется при выходе — привязана к жизненному циклу
LaunchedEffect(userId) {
repository.markSeen(userId)
}
Text(userId)
}Обратите внимание на key в LaunchedEffect(userId): поменялся userId — старая корутина отменяется, запускается новая. Это частый вопрос-уточнение: «а что будет, если элемент переиспользуется в списке?».
Ещё один блок, который любят проверять, — как связаны Compose и жизненный цикл Activity/Fragment. Короткий ответ: состояние в rememberSaveable переживает воссоздание Activity, а корутины в LaunchedEffect — нет: при выходе из композиции они отменяются.
Типичные ошибки кандидатов
Ошибки ниже я вижу на собеседованиях регулярно, даже у кандидатов с опытом 2+ года:
- «При изменении состояния пересоздаётся весь экран». Нет — пересоздаются только функции с изменившимися входными данными, остальное пропускается.
- Путают
rememberиrememberSaveable. Первое переживает рекомпозицию, второе — ещё и воссоздание Activity. «При повороте экрана состояние сбросилось» — почти всегда забытыйrememberSaveable. - Сайд-эффекты прямо в теле функции. Аналитика, запись в репозиторий, обновление ViewModel в композиции. Работает «вроде бы», но ломается при пропуске или многократном пересоздании.
- Состояние, которое живёт не там. Всё состояние экрана в одной гигантской composable вместо подъёма и статeless-компонентов — интервьюер спросит, как это тестировать.
- Не знают про ключи в
LazyColumn. Безkeyпри обновлении списка Compose не может сопоставить элементы, и пересоздаёт лишнее.
Худший вариант ответа на вопрос про жизненный цикл: «composable — это как Activity, у неё есть onCreate и onDestroy». Это не так. Фазы composable — вход, рекомпозиции, выход, — и сайд-эффекты существуют именно потому, что рекомпозиций может быть сколько угодно.
На что я смотрю, когда спрашиваю про Compose
Я Рустем Бикбулатов, senior Android-разработчик, провожу технические собеседования и менторю в Яндекс Практикуме — больше сотни учеников и разборов за плечами. Когда я спрашиваю про Compose, я не жду пересказа документации. Я жду трёх вещей: умения объяснить ментальную модель своими словами, живой пример с кодом и честное «не помню деталь, но расскажу, где это искать».
С опытом 1–2 года кандидаты обычно застревают именно на связке «состояние — жизненный цикл — сайд-эффекты»: каждый кусок знают, а в один ответ собрать не могут. Это закрывается системной подготовкой: разбор тем, тренировка ответов вслух и мок-собеседования. Я помогаю именно с этим — индивидуальное менторство под переход на middle за 4–8 недель.
Нужна помощь в переходе на middle?
Бесплатная диагностика: разберу ваши пробелы в Compose, Kotlin и Coroutines и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.
Итоги
Jetpack Compose — обязательная тема для собеседования, и проверяют её через понимание модели, а не через знание API.
- Модель: UI = f(state), однонаправленный поток данных, рекомпозиция пересоздаёт минимум.
- Состояние:
rememberпротивrememberSaveable, подъём состояния, статeless-компоненты. - Жизненный цикл: вход, рекомпозиции, выход; сайд-эффекты через
LaunchedEffectиDisposableEffect.
Главное, что нужно запомнить
Compose-собеседование выигрывает не тот, кто знает больше модификаторов, а тот, кто может объяснить, почему UI — функция состояния и что из этого следует. Всё остальное — детали.
Если на собеседованиях вы уверенно пишете экраны, но плаваете, когда начинают спрашивать «а почему оно так работает» — не нужно зубрить дальше. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.