Jetpack ComposeJuniorTechnical

В чём разница между stateful и stateless composable?

Stateful composable хранит состояние внутри через remember, stateless — получает данные параметрами и сообщает об изменениях через лямбды. Рекомендуемый паттерн: stateless-компонент + stateful-обёртка, состояние хранится во ViewModel.

Stateful и Stateless Composable

Разделение на stateful и stateless — ключевой архитектурный паттерн в Compose, называемый state hoisting (подъём состояния).

Stateful Composable

Хранит состояние внутри себя с помощью remember или rememberSaveable. Удобен для изолированных UI-компонентов, у которых состояние не нужно снаружи.

@Composable
fun ExpandableCard(title: String, body: String) {
    var expanded by remember { mutableStateOf(false) }  // состояние внутри

    Card {
        Column {
            Row(
                modifier = Modifier
                    .clickable { expanded = !expanded }
                    .padding(16.dp)
            ) {
                Text(title, Modifier.weight(1f))
                Icon(
                    if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                    contentDescription = null
                )
            }
            if (expanded) {
                Text(body, Modifier.padding(16.dp))
            }
        }
    }
}

Плюс: просто использовать. Минус: состояние закрыто — нельзя управлять снаружи, сложно тестировать.

Stateless Composable

Не содержит собственного состояния. Принимает данные через параметры и сообщает об изменениях через лямбды. Полностью предсказуем и тестируем.

@Composable
fun ExpandableCard(
    title: String,
    body: String,
    expanded: Boolean,           // состояние снаружи
    onExpandToggle: () -> Unit   // событие наружу
) {
    Card {
        Column {
            Row(
                modifier = Modifier
                    .clickable { onExpandToggle() }
                    .padding(16.dp)
            ) {
                Text(title, Modifier.weight(1f))
                Icon(
                    if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                    contentDescription = null
                )
            }
            if (expanded) {
                Text(body, Modifier.padding(16.dp))
            }
        }
    }
}

Паттерн: stateful обёртка над stateless

Типичное решение — пара функций: stateless-компонент для переиспользования и testability, и stateful-обёртка для удобства.

// Stateless — используется в сложных сценариях и тестах
@Composable
fun EmailField(
    value: String,
    onValueChange: (String) -> Unit,
    isError: Boolean = false
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        label = { Text("Email") },
        isError = isError
    )
}

// Stateful — для быстрого использования
@Composable
fun EmailFieldStateful() {
    var email by remember { mutableStateOf("") }
    EmailField(
        value = email,
        onValueChange = { email = it }
    )
}

State hoisting в ViewModel

В реальных экранах состояние поднимается в ViewModel:

class LoginViewModel : ViewModel() {
    var email by mutableStateOf("")
        private set

    fun onEmailChange(value: String) { email = value }
}

@Composable
fun LoginScreen(vm: LoginViewModel = viewModel()) {
    EmailField(
        value = vm.email,
        onValueChange = vm::onEmailChange
    )
}

Подводные камни

  • Слишком раннее поднятие состояния — состояние в ViewModel, которое нужно только локально (например, флаг «dropdown open»), загромождает ViewModel.
  • Слишком позднее поднятие — два независимых composable не могут синхронизироваться, потому что состояние у каждого своё.
  • Передача изменяемого объекта вниз вместо лямбды — нарушает принцип однонаправленного потока данных (UDF).
  • Stateful composable трудно тестировать юнит-тестами — не видно состояния снаружи.
  • Смешение бизнес-логики и UI-состояния в одном composable — переполненный компонент, сложный для поддержки.
  • Поднятие состояния слишком высоко («god state» в корневом composable) — все дочерние функции рекомпонируются при любом изменении.

Common mistakes

  • Объяснять «stateful и stateless composable» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Stateful компоненты сложнее переиспользовать, если состояние нужно синхронизировать с ViewModel или navigation.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «stateful и stateless composable» своими словами и связывает ее с кодом.
  • Называет механизм: Обычно публичный API строят stateless, а удобную stateful-обертку добавляют рядом для простых случаев.
  • Видит production-последствие: Stateful компоненты сложнее переиспользовать, если состояние нужно синхронизировать с ViewModel или navigation.

Sources

Related topics