В чём разница между 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.