State Hoisting: подъём состояния и однонаправленный поток в Compose

Автор: IT Sectr Опубликовано: 2026-06-28 Время чтения: 8 мин

State Hoisting — это паттерн в Jetpack Compose, при котором состояние выносится из дочерней Composable-функции в родительскую, а дочерняя получает данные через параметры и уведомляет об изменениях через колбэки. Это реализация принципа однонаправленного потока данных (UDF), при котором состояние поднимается наверх, а события спускаются вниз. По данным Google Android Developers, 2026, State Hoisting делает компоненты переиспользуемыми, тестируемыми и предсказуемыми.

Главное

  • State Hoisting выносит состояние из дочернего компонента в родительский
  • UDF (Unidirectional Data Flow) — состояние течёт вниз, события вверх
  • Параметры дочернего компонента: значение (T) + лямбда (T) -> Unit
  • Переиспользование — поднятое состояние позволяет использовать ту же функцию с разными источниками
  • Тестирование — State Hoisting упрощает unit-тесты, изолируя логику от UI

Что такое State Hoisting в Jetpack Compose

State Hoisting (подъём состояния) — это паттерн, при котором Composable-функция не владеет состоянием, а получает его извне. Вместо var внутри функции используются два параметра: значение для отображения и лямбда-колбэк для обработки изменений. Технически это означает, что дочерний компонент становится stateless (не имеет собственного состояния), а родитель — stateful (владеет состоянием).

Пример: компонент TextField из Material3 не хранит введённый текст внутри себя. Он принимает value: String и onValueChange: (String) -> Unit. Родитель, вызывающий TextField, объявляет var value by remember { mutableStateOf("") } и передаёт value и onValueChange. Это классический State Hoisting: TextField — dumb-компонент (просто отображает и сообщает о вводе), родитель — умный (владеет состоянием).

Stateless vs Stateful: Stateless компонент проще тестировать — он не зависит от внутреннего состояния, его поведение полностью определяется входными параметрами. Stateful компонент удобен для быстрого прототипирования, но переиспользовать его сложнее: он жёстко привязан к одному источнику данных. State Hoisting даёт выбор: любой компонент можно сделать stateless, вынеся состояние наверх.

Однонаправленный поток данных (UDF) и State Hoisting

UDF (Unidirectional Data Flow) — это архитектурный принцип, при котором данные движутся в одном направлении: от источника истины (ViewModel или родительский Composable) к UI, а события — в обратном направлении. State Hoisting — это реализация UDF на уровне отдельных компонентов. Вместо того чтобы каждый компонент сам решал, когда и как менять своё состояние, он сообщает родителю о событии, а родитель решает, как изменить состояние.

Преимущества UDF: предсказуемость — состояние меняется только в одном месте, что исключает race conditions; traceability — по стеку вызовов можно восстановить цепочку изменений; тестирование — stateful-логику можно вынести в отдельный класс и тестировать без UI. В крупных проектах UDF в сочетании с State Hoisting — стандарт де-факто.

Источник истины (Single Source of Truth) — ещё один принцип, сопутствующий UDF. Каждый фрагмент состояния имеет ровно один источник. Если два компонента используют одно и то же состояние, источник должен быть общим (на уровне ViewModel или общего родителя). State Hoisting гарантирует, что источник находится выше по иерархии, а дублирования состояния не происходит.

НаправлениеЧто передаётсяКак реализовано
Вниз (родитель → дочерний)Значение для отображенияПараметр value: T
Вверх (дочерний → родитель)Событие об измененииПараметр onValueChange: (T) -> Unit

Правила State Hoisting: когда и как поднимать состояние

Основное правило: состояние должно быть поднято на минимально возможный уровень, достаточный для всех компонентов, которым оно нужно. Если состояние используется только внутри одного компонента — оставьте его локальным. Если двум соседним компонентам нужно одно и то же состояние — поднимите в общего родителя. Если состояние нужно на всём экране — поднимите в ViewModel.

Правило минимального подъёма предотвращает ненужную сложность. Нет смысла поднимать состояние текстового поля в ViewModel, если оно используется только внутри одного экрана и не сохраняется при пересоздании Activity. Используйте rememberSaveable на уровне родителя экрана, а не ViewModel, для UI-состояния, которое должно пережить поворот экрана, но не нужно бизнес-логике.

Когда поднимать в ViewModel: если состояние должно сохраняться при пересоздании Activity, если оно необходимо нескольким экранам, если изменение состояния запускает бизнес-логику (сетевые запросы, БД). State Hoisting на уровне ViewModel — это типовой паттерн в архитектуре MVVM, где UI-слой stateless, а ViewModel — stateful.

kotlin
    // ❌ Bad: component owns its own state
@Composable
fun BadTextField(label: String) {
    var text by remember { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}

    // ✅ Good: State Hoisting — state in parent
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
    TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}

    // Usage: parent owns the state
@Composable
fun Form() {
    var name by rememberSaveable { mutableStateOf("") }
    GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}

Примеры State Hoisting в реальных компонентах

Рассмотрим экран логина, где есть два поля (email, password) и кнопка. Все три компонента получают состояние через State Hoisting: email и password управляются родителем, кнопка получает enabled-статус как значение.

kotlin
    // State Hoisting at screen level
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        // Email field — State Hoisting via lambda
        EmailField(
            email = uiState.email,
            onEmailChange = { viewModel.onEmailChanged(it) }
        )

        // Password field — similarly
        PasswordField(
            password = uiState.password,
            onPasswordChange = { viewModel.onPasswordChanged(it) }
        )

        // Button — receives only enabled (read-only)
        LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
    }
}

// Stateless component: receives email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
    OutlinedTextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        singleLine = true
    )
}

// Stateless button component
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
    Button(onClick = onClick, enabled = enabled) {
        Text("Login")
    }
}

EmailField и PasswordField — полностью stateless. Их можно переиспользовать на любом экране, подключив к любому источнику данных. LoginButton получает enabled как read-only — это ещё одна форма State Hoisting, где состояние не поднимается (кнопка не может стать enabled сама), а спускается готовым. Такой подход даёт максимальную гибкость при минимальной связанности компонентов.

State Hoisting vs локальное состояние: критерии выбора

Не каждое состояние нужно поднимать. Локальное состояние (State внутри Composable) оправдано, когда: данные нужны только внутри одного компонента, не влияют на соседние элементы, не должны переживать рекомпозицию конкретной секции. Например, состояние анимации, фокус поля ввода, текущая позиция скролла — разумно держать локально.

Когда State Hoisting необходим: состояние используется несколькими дочерними компонентами; изменение в одном дочернем должно отражаться в другом; нужно тестировать логику изменения состояния отдельно от UI; состояние должно сохраняться при пересоздании Activity. В этих случаях локальное состояние создаёт дублирование и несогласованность данных.

Гибридный подход: держите минимальное состояние локально, остальное поднимайте. Правило Compose: «поднимайте состояние настолько высоко, насколько это необходимо, и настолько низко, насколько возможно». Практически это означает начать с локального remember, и только когда появляется потребность в доступе из другого компонента — поднять уровень. Не делайте State Hoisting превентивно — это усложняет код без необходимости.

Часто задаваемые вопросы

Чем State Hoisting отличается от ViewModel?

State Hoisting — это паттерн уровня UI-компонентов. ViewModel — архитектурный слой для бизнес-логики. State Hoisting может поднимать состояние на уровень родительского Composable, на уровень экрана или в ViewModel. ViewModel — это высшая точка подъёма для состояния, которое должно переживать пересоздание Activity.

Как тестировать компонент с State Hoisting?

Stateless-компонент тестируется простой передачей значений. Вызовите Composable с нужными параметрами и проверьте отображение через ComposeTestRule. Изменение состояния проверяется на уровне родителя или ViewModel — отдельно от UI. Это значительно упрощает тесты: не нужно симулировать рекомпозицию внутри компонента.

Можно ли поднимать State только для чтения?

Да, это распространённая практика. Если компоненту нужно только отображать данные без возможности их изменить — передавайте State<T> (read-only). Компонент будет подписан на изменения, но не сможет их инициировать. Это усиливает инкапсуляцию и защищает данные от нежелательных мутаций.

Что если нужно поднять состояние на 3+ уровня вложенности?

Для глубокой передачи используйте CompositionLocal или передачу через параметры родительского Composable. Если состояние нужно на всём экране — вынесите его в ViewModel и используйте collectAsState(). Прокидывание через 5+ уровней — признак неправильной архитектуры; пересмотрите иерархию компонентов.

Влияет ли State Hoisting на производительность?

State Hoisting может незначительно увеличить количество рекомпозиций, так как изменение в родителе может перекомпоновать всех дочерних. Используйте derivedStateOf для фильтрации изменений и keys в LazyColumn для точечных обновлений. В большинстве сценариев overhead State Hoisting пренебрежимо мал по сравнению с пользой от поддерживаемости.

Итоги

  • State Hoisting выносит состояние в родительский компонент, делая дочерний stateless
  • UDF гарантирует однонаправленный поток данных: состояние вниз, события вверх
  • Переиспользование — stateless-компоненты можно подключать к любым источникам данных
  • Тестирование — UI-тесты проверяют только отображение, логика тестируется отдельно
  • Минимальный подъём — поднимайте ровно настолько, насколько нужно
  • ViewModel — высшая точка подъёма для состояния с бизнес-логикой
  • Рекомендация: начинайте с локального remember, поднимайте только при необходимости

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также