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 вызывается непредсказуемо часто.»