MutableState — це інтерфейс в Jetpack Compose, який представляє контейнер для змінюваного спостеріганого значення. Він є основою реактивної системи Compose: кожного разу, коли значення MutableState змінюється через setter, Compose Runtime сповіщає всім читаючим компонентам та запускає рекомпозицію. За даними Google Android Developers, 2026, розуміння MutableState є обов’язковим для коректної роботи зі станом в декларативному UI.
Головне
MutableState — це інтерфейс з пакета androidx.compose.runtime, який оголошує одну властивість: override var value: T. Гетер повертає поточне значення, сетер записує нове і сповіщає Compose Runtime про зміну. Інтерфейс успадковується від State<T>, де value доступне лише для читання. Така дворівнева архітектура дозволяє розділити доступ: компонент, якому потрібно лише читати значення, отримує State<T>, а компонент-власник — MutableState<T>.
Типовою реалізацією MutableState є внутрішній клас SnapshotMutableStateImpl, який використовує механізм снепшотів для відстеження змін. Коли викликається сетер value, поточний снепшот фіксує запис і позначає всі зареєстровані ObservedScope як недійсні. Ці області (зазвичай функції Composable) будуть перекомпоновані на наступному кадрі. Весь процес відбувається синхронно та без блокувань завдяки архітектурі Lock-free snapshot.
State vs MutableState: State — це інтерфейс лише для читання, що використовується для публічних API компонентів. Коли ви оголошуєте параметр функції Composable як State<Int>, ви гарантуєте, що компонент може читати, але не змінювати стан. MutableState використовується всередині компонента-власника. Такий розподіл є однією з базових практик Compose, що запобігає несанкціонованим змінам.
Єрархія інтерфейсів станів в Compose має кілька рівнів. На вершині знаходиться State<T> зі значенням лише для читання. Нижче знаходиться MutableState<T> зі значенням для читання-запису. Далі йдуть спеціалізовані примітивні версії: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState та інші, які уникають автопакування (боксингу) примітивів.
MutableDoubleState та MutableLongState менш поширені, але теж існують. Інтерфейси колекцій: MutableListState — для відстеження змін всередині списку, MutableStateMap — для меп. Кожен з цих інтерфейсів оптимізований для конкретного сценарію та розширює базовий MutableState додатковими методами роботи з колекціями.
SnapshotStateList та SnapshotStateMap — це реалізації змінюваних списків та меп, сумісні зі снепшотами. Вони дозволяють відстежувати не лише заміну значення, але й внутрішні зміни: додавання елемента до списку, видалення, зміну існуючого елемента. Для таких структур mutableStateListOf() та mutableStateMapOf() створюють відповідні спостерігувані колекції.
| Інтерфейс | Призначення | Метод створення |
|---|---|---|
| State<T> | Контейнер лише для читання | — |
| MutableState<T> | Контейнер для читання-запису | mutableStateOf() |
| MutableIntState | Примітивний Int без боксингу | mutableIntStateOf() |
| MutableFloatState | Примітивний Float без боксингу | mutableFloatStateOf() |
| SnapshotStateList | Спостерігуваний список | mutableStateListOf() |
| SnapshotStateMap | Спостерігувана мепа | mutableStateMapOf() |
SnapshotMutationPolicy — це інтерфейс, який визначає, коли зміна в MutableState вважається значущою. mutableStateOf приймає policy другим аргументом. Стандартні реалізації: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (завжди вважає зміну значущою). Для користувацької логіки можна реалізувати власну політику.
structuralEquality() — типова поведінка. Compose порівнює нове значення зі старим через equals(). Якщо результат true — рекомпозиція НЕ запускається. Це зручно для примітивів та data класів, де два екземпляри з однаковими полями вважаються рівними. Проблема: якщо data клас містить List, equals() виконує глибоке порівняння, що може бути затратним для великих списків.
referentialEquality() — порівнює посилання через ===. Рекомпозиція запускається лише при призначенні іншого об’єкта, навіть якщо вміст ідентичний. Це оптимально для незмінних data класів, де кожен новий екземпляр гарантує зміну. neverEqualPolicy() — завжди вважає зміну значущою без виконання порівняння. Корисно, коли сетер викликається рідко і немає потреби витрачати час на equals.
// Policy comparison in practice
data class User(val name: String, val age: Int)
@Composable
fun UserProfile() {
// structuralEquality: recomposition ONLY if data changed
var user1 by remember {
mutableStateOf(User("Alice", 30))
}
// referentialEquality: recomposition on ANY assignment
var user2 by remember {
mutableStateOf(User("Bob", 25),
SnapshotMutationPolicy.referentialEquality())
}
// user1: copy() with same fields does NOT trigger recomposition
// user2: even user2.copy() == user2 triggers recomposition (new ref)
}
Примітивні MutableIntState та аналоги — це спеціалізовані інтерфейси, які зберігають примітиви без автопакування. Звичайний MutableState<Int> зберігає Int як Integer, що при кожному записі створює об’єкт на купі. MutableIntState зберігає int (примітив), повністю усуваючи накладні витрати на боксинг. Це особливо важливо для високочастотних оновлень — лічильників, позицій прокрутки, анімаційних значень.
mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — функції, що створюють примітивні MutableState. Інтерфейси називаються MutableIntState, MutableFloatState, MutableLongState. Вони розширюють MutableState<Int>, MutableState<Float> та MutableState<Long> відповідно, додаючи властивість intValue для швидкого доступу до примітиву. Їхня внутрішня реалізація використовує AtomicInteger для безблокового читання/запису.
Застосування: лічильники (Int), позиції прокрутки (Float offset), часові мітки (Long). У більшості повсякденних сценаріїв різниця в продуктивності непомітна, але в LazyList з тисячами елементів та анімацією переходів примітивні State дають відчутний приріст. Google рекомендує використовувати примітивні State для типових сценаріїв замість універсального mutableStateOf.
@Composable
fun ScrollCounter() {
// Bad: boxing on every update
var badCount by remember { mutableStateOf(0) }
// Good: no boxing, primitive storage
var goodCount by remember { mutableIntStateOf(0) }
// Usage is identical
Button(onClick = { goodCount++ }) {
Text("Count: $goodCount")
}
}
Розглянемо компонент TodoList, де MutableState використовується в двох формах: як окремі змінні для стану введення та як SnapshotStateList для динамічного списку завдань. Обидва використовують делегування для стислості коду.
data class TodoItem(val id: Int, val text: String, val isDone: Boolean = false)
@Composable
fun TodoScreen() {
var inputText by remember { mutableStateOf("") }
val items = remember { mutableStateListOf() }
Column(modifier = Modifier.padding(16.dp)) {
Row {
TextField(
value = inputText,
onValueChange = { inputText = it }
)
Button(onClick = {
if (inputText.isNotBlank()) {
items.add(TodoItem(items.size, inputText))
inputText = ""
}
}) { Text("Add") }
}
LazyColumn {
items(items) { item ->
Row(modifier = Modifier.fillMaxWidth().clickable {
val idx = items.indexOf(item)
items[idx] = item.copy(isDone = !item.isDone)
}) {
Checkbox(checked = item.isDone, onCheckedChange = null)
Text(item.text)
}
}
}
}
}
mutableStateListOf створює SnapshotStateList — змінюваний список, який відстежує зміни окремих елементів. При виклику items.add() та items[n] = newValue Compose бачить мутацію та перекомпоновує лише ті елементи LazyColumn, які змінилися. inputText — це звичайний MutableState<String>. Комбінація двох типів MutableState (одиночний та колекція) — типовий патерн для екранів з формами та списками.
Часто задавані питання
MutableState без remember буде створюватися заново при кожній рекомпозиції. Кожен новий виклик mutableStateOf створює новий об’єкт, а старе значення губиться. Завжди використовуйте remember для збереження State між рекомпозиціями, якщо тільки State не створюється поза Composable (наприклад, в ViewModel).
Прочитайте .value один раз поза снепшотом через snapshot { }. Але це вимикає реактивність — зміни більше не будуть викликати рекомпозицію. Для одноразового читання без підписки використовуйте currentValue() всередині snapshot без читання.
mutableIntStateOf швидше, оскільки не потребує пакування int в Integer. При тисячах оновлень на секунду (анімація, прокрутка) різниця може сягати 30-50% часу виділення. Для рідкісних оновлень (кліки, введення тексту) різниця незначна.
Можна, але не рекомендується. Замість MutableState передавайте State (лише для читання) + лямбду onValueChange. Це реалізує патерн State Hoisting і робить компонент перевикористовуваним. Компоненти, що приймають MutableState, порушують однонапрявлений потік даних.
Реалізуйте інтерфейс MutableState та надайте override var value з гетером та сетером. У сетері можна додати валідацію або логування. Для зворотної сумісності з Compose Runtime обгорніть свою реалізацію в snapshotFlow або використовуйте snapshotIncrement.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також