State Hoisting — це патерн у Jetpack Compose, при якому стан виноситься з дочірньої Composable-функції в батьківську, а дочірня отримує дані через параметри та повідомляє про зміни через колбеки. Це реалізація принципу односпрямованого потоку даних (UDF), при якому стан піднімається вгору, а події спускаються вниз. За даними Google Android Developers, 2026, State Hoisting робить компоненти перевикористовуваними, тестованими та передбачуваними.
Головне
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 (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 |
Основне правило: стан має бути піднятий на мінімально можливий рівень, достатній для всіх компонентів, яким він потрібен. Якщо стан використовується тільки всередині одного компонента — залиште його локальним. Якщо двом сусіднім компонентам потрібен один і той же стан — підніміть у спільного батька. Якщо стан потрібен на всьому екрані — підніміть у ViewModel.
Правило мінімального підняття запобігає непотрібній складності. Немає сенсу піднімати стан текстового поля у ViewModel, якщо він використовується тільки всередині одного екрана і не зберігається при перестворенні Activity. Використовуйте rememberSaveable на рівні батька екрана, а не ViewModel, для UI-стану, який має пережити поворот екрана, але не потрібен бізнес-логіці.
Коли піднімати в ViewModel: якщо стан має зберігатися при перестворенні Activity, якщо він необхідний кільком екранам, якщо зміна стану запускає бізнес-логіку (мережеві запити, БД). State Hoisting на рівні ViewModel — це типовий патерн в архітектурі MVVM, де UI-шар stateless, а ViewModel — stateful.
// ❌ Погано: компонент володіє власним станом
@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")
}
Розглянемо екран логіну, де є два поля (email, password) і кнопка. Всі три компоненти отримують стан через State Hoisting: email і password керуються батьком, кнопка отримує enabled-статус як значення.
// 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 всередині Composable) виправданий, коли: дані потрібні тільки всередині одного компонента, не впливають на сусідні елементи, не повинні переживати рекомпозицію конкретної секції. Наприклад, стан анімації, фокус поля введення, поточна позиція скролу — розумно тримати локально.
Коли State Hoisting необхідний: стан використовується кількома дочірніми компонентами; зміна в одному дочірньому має відображатися в іншому; потрібно тестувати логіку зміни стану окремо від UI; стан має зберігатися при перестворенні Activity. У цих випадках локальний стан створює дублювання та неузгодженість даних.
Гібридний підхід: тримайте мінімальний стан локально, решту піднімайте. Правило Compose: «піднімайте стан настільки високо, наскільки це необхідно, і настільки низько, наскільки можливо». Практично це означає почати з локального remember, і тільки коли з'являється потреба в доступі з іншого компонента — підняти рівень. Не робіть State Hoisting превентивно — це ускладнює код без необхідності.
Часто задавані питання
State Hoisting — це патерн рівня UI-компонентів. ViewModel — архітектурний шар для бізнес-логіки. State Hoisting може піднімати стан на рівень батьківського Composable, на рівень екрана або в ViewModel. ViewModel — це найвища точка підняття для стану, який має переживати перестворення Activity.
Stateless-компонент тестується простою передачею значень. Викличте Composable з потрібними параметрами та перевірте відображення через ComposeTestRule. Зміна стану перевіряється на рівні батька або ViewModel — окремо від UI. Це значно спрощує тести: не потрібно симулювати рекомпозицію всередині компонента.
Так, це поширена практика. Якщо компоненту потрібно тільки відображати дані без можливості їх змінити — передавайте State<T> (read-only). Компонент буде підписаний на зміни, але не зможе їх ініціювати. Це посилює інкапсуляцію та захищає дані від небажаних мутацій.
Для глибокої передачі використовуйте CompositionLocal або передачу через параметри батьківського Composable. Якщо стан потрібен на всьому екрані — винесіть його в ViewModel і використовуйте collectAsState(). Прокидання через 5+ рівнів — ознака неправильної архітектури; перегляньте ієрархію компонентів.
State Hoisting може незначно збільшити кількість рекомпозицій, оскільки зміна в батьківському може перекомпонувати всіх дочірніх. Використовуйте derivedStateOf для фільтрації змін і keys у LazyColumn для точкових оновлень. У більшості сценаріїв overhead State Hoisting є незначним порівняно з користю від підтримуваності.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також