Jetpack Compose против View: что спрашивают

Вопрос выглядит как «что вам больше нравится», но проверяет понимание принципиальной разницы двух моделей UI.

Декларативный UI — вы описываете, как экран должен выглядеть при данном состоянии, а не какими командами привести его в нужный вид. Синхронизацией занимается фреймворк.

Вопрос 1: «Чем Compose принципиально отличается от View?»

Что проверяет интервьюер

Понимаете ли вы, что дело не в синтаксисе. Ответ «в Compose не нужен XML и findViewById» — поверхностный.

Развёрнутый ответ

Система View императивна: есть дерево объектов с внутренним состоянием, и вы меняете его командами — setText, setVisibility, notifyItemChanged. Состояние живёт в двух местах сразу: в вашей модели и внутри виджетов. Их рассинхрон — источник багов вида «после поворота чекбокс сбросился, а данные нет».

Compose декларативен: composable-функция получает состояние и описывает результат. При изменении состояния функция вызывается заново — это и есть рекомпозиция. Единственное место хранения состояния — ваша модель.

// View: императивно указываем, ЧТО изменить
fun render(state: UiState) {
    binding.progress.isVisible = state.loading
    binding.title.text = state.title
    binding.retry.isVisible = state.error != null
}

// Compose: описываем, КАК выглядит экран при данном состоянии
@Composable
fun Screen(state: UiState, onRetry: () -> Unit) {
    when {
        state.loading -> CircularProgressIndicator()
        state.error != null -> ErrorBlock(state.error, onRetry)
        else -> Text(state.title)
    }
}

Технические отличия, которые стоит назвать:

  • В Compose нет иерархии View — есть дерево узлов LayoutNode, которое ведёт композитор. Инфляции XML тоже нет.
  • Измерение однопроходное: composable не может измерять ребёнка дважды. Это устраняет квадратичное поведение, которым славились вложенные RelativeLayout и LinearLayout с весами. Если нужно «померить и решить», используется SubcomposeLayout или intrinsic-измерения — и оба стоят дороже.
  • Compose работает поверх той же Activity и совместим с View: ComposeView кладёт Compose внутрь XML, AndroidView — наоборот. Так мигрируют по экрану, а не всё сразу.

Вопрос 2: «Что такое state hoisting и чем remember отличается от rememberSaveable?»

Что проверяет интервьюер

Умеете ли вы проектировать composable как переиспользуемые и тестируемые, а не «каждый со своим состоянием внутри».

Развёрнутый ответ

State hoisting — подъём состояния к вызывающему. Composable становится stateless: принимает значение и колбэк изменения, ничего не хранит. Такой компонент легко переиспользовать, тестировать и превьюить.

// stateful: состояние внутри, снаружи им не управлять
@Composable
fun SearchField() {
    var query by remember { mutableStateOf("") }
    TextField(value = query, onValueChange = { query = it })
}

// stateless: состояние поднято к вызывающему
@Composable
fun SearchField(query: String, onQueryChange: (String) -> Unit) {
    TextField(value = query, onValueChange = onQueryChange)
}

remember сохраняет значение между рекомпозициями, но теряет его при смене конфигурации. rememberSaveable дополнительно пишет значение в Bundle — переживает поворот и смерть процесса, но требует, чтобы тип был Parcelable или имел свой Saver. Настоящее состояние экрана всё равно должно жить во ViewModel: rememberSaveable — для локальных вещей вроде «раскрыт ли аккордеон».

Вопрос 3: «Почему нельзя делать побочные эффекты прямо в теле composable?»

Потому что тело composable вызывается непредсказуемо часто, в любом порядке и может быть отброшено. Запрос в сеть, написанный прямо в теле, отправится столько раз, сколько случится рекомпозиций.

// ПЛОХО: запрос уйдёт при каждой рекомпозиции
@Composable
fun Screen(vm: FeedViewModel) {
    vm.load()
    ...
}

// ХОРОШО: эффект привязан к ключу и живёт вместе с композицией
@Composable
fun Screen(userId: Long, vm: FeedViewModel) {
    LaunchedEffect(userId) {
        vm.load(userId)      // перезапустится только при смене userId
    }
}
ЭффектКогда нужен
LaunchedEffect(key)запустить корутину при входе в композицию или смене ключа
DisposableEffect(key)подписка, которую надо снять при выходе (onDispose)
rememberCoroutineScope()запустить корутину из колбэка, например по клику
SideEffectотдать значение не-Compose коду после успешной композиции

Типичные ошибки кандидатов

  • «Compose — просто новый способ верстать вместо XML». Из этого не следует ничего про рекомпозицию и состояние.
  • Не знают про ComposeView/AndroidView и считают, что миграция возможна только целиком.
  • Хранят состояние экрана в remember вместо ViewModel — оно теряется при повороте.
  • Утверждают, что Compose «всегда быстрее». Быстрее там, где иерархия View была глубокой; на простых экранах разница невелика, а первый запуск даже медленнее без baseline profiles.

Как ответить кратко (20–30 секунд)

«Система View императивна: состояние живёт и в модели, и внутри виджетов, а вы синхронизируете их вручную. Compose декларативен: composable — функция от состояния, при изменении состояния она перевызывается, и единственный источник состояния — модель. Технически в Compose нет иерархии View и инфляции XML, а измерение однопроходное, поэтому нет квадратичного поведения вложенных layout. State hoisting — подъём состояния к вызывающему, чтобы компоненты были stateless и переиспользуемыми. remember переживает рекомпозицию, rememberSaveable — ещё и поворот, но состояние экрана всё равно держу во ViewModel. Побочные эффекты — только в LaunchedEffect или DisposableEffect, потому что тело composable вызывается непредсказуемо часто.»

Проверьте себя
1. В чём главное принципиальное отличие Compose от системы View?
ACompose не требует XML, всё пишется на Kotlin
BCompose декларативен: UI — функция от состояния, и фреймворк сам приводит экран в нужный вид
CCompose рендерит через OpenGL, а View — через Canvas
DCompose работает без Activity
2. Что произойдёт, если вызвать viewModel.load() прямо в теле composable-функции?
Aзагрузка выполнится ровно один раз при первом показе
Bкомпилятор Compose выдаст ошибку сборки
Cвызов будет повторяться при каждой рекомпозиции — нужен LaunchedEffect
Dвызов выполнится после выхода из композиции
3. Чем rememberSaveable отличается от remember?
ArememberSaveable сохраняет значение в Bundle и переживает смену конфигурации и смерть процесса
BrememberSaveable кэширует значение на диске
CrememberSaveable работает только внутри LazyColumn
Dразницы нет, это псевдонимы