MutableState — спостеріганий стан та механізм оновлення в Compose

Автор: IT Sectr Опубліковано: 2026-06-28 Час читання: 7 хв

MutableState — це інтерфейс в Jetpack Compose, який представляє контейнер для змінюваного спостеріганого значення. Він є основою реактивної системи Compose: кожного разу, коли значення MutableState змінюється через setter, Compose Runtime сповіщає всім читаючим компонентам та запускає рекомпозицію. За даними Google Android Developers, 2026, розуміння MutableState є обов’язковим для коректної роботи зі станом в декларативному UI.

Головне

  • MutableState — інтерфейс compose.runtime з однією властивістю value (гетер + сетер)
  • State — батьківський інтерфейс лише для читання, MutableState додає можливість запису
  • Рекомпозиція запускається при виклику сетера value в межах активного циклу snapshot
  • SnapshotMutationPolicy визначає, коли зміна вважається значущою для рекомпозиції
  • MutableIntState та аналоги — оптимізовані примітивні версії MutableState

Що таке MutableState в Jetpack Compose

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, що запобігає несанкціонованим змінам.

Єрархія State, MutableState та похідних інтерфейсів

Єрархія інтерфейсів станів в 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: коли запускати рекомпозицію

SnapshotMutationPolicy — це інтерфейс, який визначає, коли зміна в MutableState вважається значущою. mutableStateOf приймає policy другим аргументом. Стандартні реалізації: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (завжди вважає зміну значущою). Для користувацької логіки можна реалізувати власну політику.

structuralEquality() — типова поведінка. Compose порівнює нове значення зі старим через equals(). Якщо результат true — рекомпозиція НЕ запускається. Це зручно для примітивів та data класів, де два екземпляри з однаковими полями вважаються рівними. Проблема: якщо data клас містить List, equals() виконує глибоке порівняння, що може бути затратним для великих списків.

referentialEquality() — порівнює посилання через ===. Рекомпозиція запускається лише при призначенні іншого об’єкта, навіть якщо вміст ідентичний. Це оптимально для незмінних data класів, де кожен новий екземпляр гарантує зміну. neverEqualPolicy() — завжди вважає зміну значущою без виконання порівняння. Корисно, коли сетер викликається рідко і немає потреби витрачати час на equals.

kotlin
    // 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)
}

Примітивні State: MutableIntState, MutableFloatState, MutableLongState

Примітивні 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.

kotlin
@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")
    }
}

Практичні приклади роботи з MutableState

Розглянемо компонент TodoList, де MutableState використовується в двох формах: як окремі змінні для стану введення та як SnapshotStateList для динамічного списку завдань. Обидва використовують делегування для стислості коду.

kotlin
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?

MutableState без remember буде створюватися заново при кожній рекомпозиції. Кожен новий виклик mutableStateOf створює новий об’єкт, а старе значення губиться. Завжди використовуйте remember для збереження State між рекомпозиціями, якщо тільки State не створюється поза Composable (наприклад, в ViewModel).

Як перетворити MutableState в звичайну змінну?

Прочитайте .value один раз поза снепшотом через snapshot { }. Але це вимикає реактивність — зміни більше не будуть викликати рекомпозицію. Для одноразового читання без підписки використовуйте currentValue() всередині snapshot без читання.

Що швидше: mutableStateOf чи mutableIntStateOf?

mutableIntStateOf швидше, оскільки не потребує пакування int в Integer. При тисячах оновлень на секунду (анімація, прокрутка) різниця може сягати 30-50% часу виділення. Для рідкісних оновлень (кліки, введення тексту) різниця незначна.

Чи можна використовувати MutableState як аргумент функції Composable?

Можна, але не рекомендується. Замість MutableState передавайте State (лише для читання) + лямбду onValueChange. Це реалізує патерн State Hoisting і робить компонент перевикористовуваним. Компоненти, що приймають MutableState, порушують однонапрявлений потік даних.

Як створити кастомну реалізацію MutableState?

Реалізуйте інтерфейс MutableState та надайте override var value з гетером та сетером. У сетері можна додати валідацію або логування. Для зворотної сумісності з Compose Runtime обгорніть свою реалізацію в snapshotFlow або використовуйте snapshotIncrement.

Підсумки

  • MutableState — базовий інтерфейс для змінюваного спостеріганого стану в Compose
  • State — версія лише для читання для передачі даних без права зміни
  • SnapshotMutationPolicy керує умовами запуску рекомпозиції при зміні
  • Примітивні State (MutableIntState та ін.) усувають накладні витрати на боксинг
  • SnapshotStateList та SnapshotStateMap відстежують внутрішні зміни колекцій
  • Рекомпозиція запускається автоматично при будь-якому виклику сетера value
  • Рекомендація: використовуйте MutableState для володіння станом та State для передачі вниз

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

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

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