State hoisting и однонаправленный поток данных
State hoisting — это перенос состояния из composable наверх к вызывающему, что делает компонент stateless, переиспользуемым и тестируемым, и реализует однонаправленный поток данных.
Суть: состояние спускается вниз, события поднимаются вверх — этот однонаправленный поток данных делает поведение интерфейса предсказуемым и упрощает отладку.
Если каждый composable хранит своё состояние внутри, его трудно переиспользовать и тестировать, а данные расползаются по дереву. Паттерн state hoisting решает это: состояние поднимают к общему предку, а composable получает значение и колбэк для его изменения параметрами. Composable, не хранящий состояние, называется stateless.
// stateless: состояние и колбэк приходят снаружи
@Composable
fun CounterDisplay(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Нажато: $count раз")
}
}
// stateful: владеет состоянием и передаёт его вниз
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
CounterDisplay(count = count, onIncrement = { count++ })
}Обратите внимание на схему: значение count спускается вниз как параметр, а событие «увеличить» поднимается вверх через колбэк onIncrement. Сам CounterDisplay ничего не знает о том, откуда берётся состояние, — его можно переиспользовать где угодно и легко протестировать.
Однонаправленный поток данных
Этот принцип называют unidirectional data flow (UDF): данные текут сверху вниз (state down), события — снизу вверх (events up). Состояние всегда меняется в одном месте — у владельца, — поэтому всегда понятно, кто и почему его изменил.
Однонаправленный поток данных (UDF)
CounterScreen (владелец состояния)
| state down: count
v
CounterDisplay (stateless)
| events up: onIncrement()
^
+---- клик пользователя
Состояние меняется ТОЛЬКО у владельцаГде хранить состояние
Поднимать состояние нужно до ближайшего общего предка всех composable, которым оно нужно, — но не выше. Слишком высокий подъём усложняет дерево, слишком низкий мешает переиспользованию. Для состояния, переживающего поворот и относящегося к бизнес-логике, владельцем становится ViewModel — об этом следующий урок.
Как работает под капотом
Когда состояние поднято, stateless-composable получает значение как обычный параметр. При изменении состояния у владельца происходит рекомпозиция, и новое значение спускается вниз как новый аргумент. Stateless-composable не подписан на состояние напрямую — он просто перерисовывается с новым параметром. Это делает его чистой функцией от входных данных, что и обеспечивает предсказуемость и тестируемость.
Частые ошибки
Хранить состояние слишком низко. Если двум соседним composable нужно одно состояние, его нужно поднять к их общему предку, а не дублировать.
Менять состояние из stateless-компонента напрямую. Stateless-компонент должен лишь сообщать о событии через колбэк, а не лезть в чужое состояние.
Поднимать состояние слишком высоко. Глобальное состояние ради одной кнопки усложняет дерево и провоцирует лишние рекомпозиции.
Best practices
- Делайте переиспользуемые composable stateless: состояние и колбэки — параметрами.
- Поднимайте состояние до ближайшего общего предка, но не выше.
- Соблюдайте UDF: состояние вниз, события вверх.
- Для бизнес-состояния владельцем делайте ViewModel.
Принцип «состояние вниз, события вверх» удобно смоделировать на Python: владелец хранит состояние, отображение лишь рисует и шлёт события. Запустите врезку.
# Аналог state hoisting и UDF из Compose
def counter_display(count):
# stateless: только рисует то, что передали
return 'Кнопка [ Нажато: ' + str(count) + ' раз ]'
# владелец состояния
count = 0
print(counter_display(count))
def on_increment(c): # событие поднимается к владельцу
return c + 1
for _ in range(3):
count = on_increment(count) # меняет ТОЛЬКО владелец
print(counter_display(count))Попробуй сам ▶ — добавьте событие сброса (on_reset), которое возвращает 0. Заметьте: отображение никогда не меняет состояние само — только владелец.
Итог: state hoisting разделяет компоненты на stateful и stateless и реализует однонаправленный поток данных, делая UI предсказуемым. Логичный владелец бизнес-состояния — ViewModel, к которой мы переходим.