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; простежуваність — за стеком викликів можна відновити ланцюжок змін; тестування — 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
    // ❌ Погано: компонент володіє власним станом
@Composable
fun BadTextField(label: String) {
    var text by remember { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}

    // ✅ Добре: State Hoisting — стан у батька
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
    TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}

    // Використання: батько володіє станом
@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 на рівні екрана
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        // Поле Email — State Hoisting через лямбду
        EmailField(
            email = uiState.email,
            onEmailChange = { viewModel.onEmailChanged(it) }
        )

        // Поле Пароль — аналогічно
        PasswordField(
            password = uiState.password,
            onPasswordChange = { viewModel.onPasswordChanged(it) }
        )

        // Кнопка — отримує тільки enabled (read-only)
        LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
    }
}

// Stateless компонент: отримує email + колбек
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
    OutlinedTextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        singleLine = true
    )
}

// Stateless компонент кнопки
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
    Button(onClick = onClick, enabled = enabled) {
        Text("Увійти")
    }
}

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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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