mutableStateOf — це функція в Jetpack Compose, яка створює контейнер змінюваного спостережуваного стану. Коли значення всередині цього контейнера змінюється, Compose автоматично запускає рекомпозицію всіх компонентів, що читають цей стан. Без mutableStateOf інтерфейс не міг би реактивно оновлюватися при змінах даних. За даними Google Android Developers, 2026, mutableStateOf є основним будівельним блоком для локального стану в Compose.
Головне
mutableStateOf — це функція з пакета compose.runtime, яка створює об'єкт MutableState<T>, що зберігає значення та здатен повідомляти Compose Runtime про зміни. Сигнатура: fun <T> mutableStateOf(value: T, policy: SnapshotMutationPolicy<T> = structuralEquality()): MutableState<T>. Параметр policy визначає, коли зміна вважається значущою — при структурній рівності, при посилальній рівності або ніколи.
MutableState — це інтерфейс з єдиною властивістю value: геттер для читання та сеттер для запису. При виклику сеттера Compose Runtime фіксує зміну в snapshot та позначає всі Composable-функції, що читають цю State-змінну, як такі, що потребують рекомпозиції. Цей процес відбувається синхронно в межах одного snapshot-циклу, що виключає проміжні стани при каскадних змінах.
Параметр policy — другий аргумент mutableStateOf, що визначає поведінку порівняння. structuralEquality() перевіряє equals() — це поведінка за замовчуванням. referentialEquality() перевіряє === (посилальна рівність). neverEqual() вважає кожне присвоєння зміною. Вибір policy впливає на те, чи запускатиметься рекомпозиція при присвоєнні того самого значення.
Найпростіший спосіб оголосити спостережуваний стан — використовувати mutableStateOf з remember. Без remember при кожній рекомпозиції створювався б новий State, і всі попередні зміни втрачалися б. remember гарантує, що один і той самий MutableState переживає серію рекомпозицій, поки Composable-функція залишається в складі композиції.
@Composable
fun Counter() {
// Без делегування: читання/запис через .value
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
@Composable
fun CounterDelegated() {
// З делегуванням: var + by = Property Delegation
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
Різниця між двома варіантами — синтаксична. Property Delegation (by) використовує угоду Kotlin: компілятор генерує виклики getValue() та setValue() для читання та запису. Це еквівалентно прямому зверненню до count.value, але виглядає як робота зі звичайною змінною. Обидва варіанти функціонально ідентичні: Compose відстежує читання в геттері та запис у сеттері незалежно від форми запису.
| Форма | Код | Читання | Запис |
|---|---|---|---|
| Без делегування | val count = mutableStateOf(0) | count.value | count.value = n |
| З делегуванням | var count by mutableStateOf(0) | count | count = n |
Механізм делегованих властивостей Kotlin — це не особливість Compose, а вбудована можливість мови. Будь-який клас може реалізувати оператори getValue(thisRef, property) та setValue(thisRef, property, value), після чого його екземпляр можна використовувати з ключовим словом by. MutableState саме так і працює: getValue повертає поточне значення, а setValue присвоює нове.
Важлива відмінність: val та var. mutableStateOf можна присвоїти як val, так і var. У випадку val (val count = mutableStateOf(0)) сам об'єкт MutableState незмінний, але його властивість value можна змінювати. У випадку var (var count by mutableStateOf(0)) делегування створює ілюзію роботи з примітивом, але насправді сеттер викликає setValue на MutableState. Вибір між val та var — це вибір між явним та неявним доступом до .value.
State Delegation — це синтаксичний цукор, який спрощує код, але не змінює механіку. Компілятор Kotlin транслює var x by mutableStateOf(0) в getter/setter, які викликають mutableStateOf.getValue() та mutableStateOf.setValue(). У згенерованому байткоді різниці між val та var з by немає — обидва працюють через один і той самий MutableState-контейнер.
// Користувацький делегат для Compose State
class ValidatedState<T>(initialValue: T) {
private val state = mutableStateOf(initialValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (value != state.value) {
state.value = value
}
}
}
@Composable
fun Test() {
var text by remember { ValidatedState("") }
}
Snapshot — це механізм Compose Runtime, що забезпечує консистентність читання State при паралельних змінах. Коли Composable-функція читає mutableStateOf, знімок фіксує поточне значення. Якщо під час композиції інша зміна записує в той самий State, знімок бачить запис, але не дозволяє читати неузгоджені дані — читання завжди повертає значення, актуальне на момент початку знімка.
При виклику сеттера mutableStateOf.value = newValue Compose Runtime не одразу запускає рекомпозицію. Натомість зміна реєструється в поточному snapshot. Коли snapshot застосовується (на межі кадру), Compose проходить по списку змінених State та позначає компоненти, що читають, як Invalid. Тільки в наступному кадрі запускається рекомпозиція. Це гарантує, що інтерфейс не перемальовується десятки разів при каскадних змінах.
Глобальні та локальні snapshot: за замовчуванням mutableStateOf працює в глобальному snapshot, що застосовується автоматично. Можна створити локальний snapshot через Snapshot.takeSnapshot() для ізольованого читання без побічних ефектів. Це використовується всередині Modifier, де потрібно прочитати State, але не підписуватися на зміни. Такий підхід оптимізує продуктивність і запобігає неочікуваним рекомпозиціям.
Розглянемо реальний сценарій — форма логіна з трьома полями: email, пароль та статус завантаження. Усі три поля використовують mutableStateOf, але з різними policy та різною вкладеністю. email використовує делегування, password — прямий доступ.
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun LoginForm(onLogin: (String, String) -> Unit) {
// Єдиний State для форми, policy = referentialEquality
var formState by remember {
mutableStateOf(LoginState(), SnapshotMutationPolicy.referentialEquality())
}
val isValid = remember(formState) {
formState.email.contains("@") && formState.password.length() >= 6
}
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = formState.email,
onValueChange = { formState = formState.copy(email = it) },
label = { Text("Електронна пошта") }
)
OutlinedTextField(
value = formState.password,
onValueChange = { formState = formState.copy(password = it) },
label = { Text("Пароль") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = { onLogin(formState.email, formState.password) },
enabled = isValid
) {
Text("Увійти")
}
}
}
У цьому прикладі mutableStateOf використовується з кастомним data-класом LoginState та policy referentialEquality. Це означає, що рекомпозиція запуститься тільки при присвоєнні нового екземпляра LoginState через copy(). isValid обчислюється на основі formState та перераховується тільки при його зміні. Такий підхід дає чіткий контроль над рекомпозиціями: кожне поле форми змінюється тільки через створення нової копії.
Часті запитання
mutableStateOf — це Compose-специфічний контейнер, що працює всередині snapshot. StateFlow — з kotlinx.coroutines.flow, який не прив'язаний до Compose. mutableStateOf автоматично запускає рекомпозицію, StateFlow вимагає collectAsState(). Для UI-стану всередині Composable переважніший mutableStateOf.
Так, mutableStateOf можна викликати поза Composable-функцією, але він не відстежуватиметься. Для роботи реактивності в інтерфейсі потрібно читати State всередині Composable. Багато ViewModel використовують MutableStateField (обгортку над mutableStateOf) для передачі стану в інтерфейс через StateFlow.
Snapshot система гарантує консистентність: кожна рекомпозиція бачить узгоджений стан на момент початку знімка. Зміни з різних потоків застосовуються атомарно на межі кадру, що виключає race condition при читанні в межах однієї композиції.
Присвойте нове значення: count.value = 0 (або count = 0 при делегуванні). Якщо потрібне повне перестворення State — використовуйте remember з ключем: remember(key) { mutableStateOf(initial) } — при зміні ключа State буде створено заново.
Композер використовує snapshot, які групують зміни: навіть при сотні присвоєнь в одному кадрі рекомпозиція виконується лише один раз. Для надто частих оновлень (анімації) використовуйте Animatable або animate*AsState — вони оптимізовані для покадрових оновлень.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також