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, к которой мы переходим.

Проверьте себя
1. Что означает паттерn state hoisting в Compose?
AХранение всего состояния в глобальной переменной
BПодъём состояния из composable к вызывающему: значение приходит параметром, а изменения — через колбэк
CПолный отказ от состояния
DСохранение состояния в базу данных
2. Как формулируется принцип однонаправленного потока данных (UDF)?
AСобытия вниз, состояние вверх
BСостояние вниз (state down), события вверх (events up)
CСостояние и события движутся в обе стороны свободно
DСостояние хранится в каждом компоненте отдельно