MutableState — наблюдаемое состояние и механизм обновления в Compose

Автор: IT Sectr Опубликовано: 2026-06-28 Время чтения: 7 мин

MutableState — это интерфейс в Jetpack Compose, который представляет контейнер для изменяемого наблюдаемого значения. Он является основой реактивной системы Compose: каждый раз, когда значение MutableState меняется через сеттер, 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, у которого value доступен только для чтения. Такая двухуровневая архитектура позволяет разделять доступ: компонент, которому нужно только читать значение, получает State, а компонент-владелец — MutableState.

Реализацией MutableState по умолчанию является внутренний класс SnapshotMutableStateImpl, который использует механизм снапшотов для отслеживания изменений. Когда сеттер value вызывается, текущий снапшот фиксирует запись и помечает все зарегистрированные ObservedScope (области наблюдения) как невалидные. Эти области (обычно Composable-функции) будут перекомпонованы на следующем кадре. Весь процесс происходит синхронно и без блокировок благодаря архитектуре Lock-free snapshot.

State vs MutableState: State — это интерфейс только для чтения, используемый для публичных API компонентов. Когда вы объявляете параметр Composable-функции как State<Int>, вы гарантируете, что компонент может читать, но не менять состояние. MutableState используется внутри компонента-владельца. Такое разделение — одна из базовых практик Compose, предотвращающая несанкционированные изменения.

Иерархия State, MutableState и производных интерфейсов

Иерархия интерфейсов состояний в Compose имеет несколько уровней. На вершине — State<T> с read-only value. Ниже — MutableState<T> с read-write value. Далее идут специализированные примитивные версии: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState и другие, которые избегают автоупаковки (boxing) примитивов.

MutableDoubleState и MutableLongState — менее распространённые, но тоже существующие типы. Интерфейсы коллекций: MutableListState — для отслеживания изменений внутри списка, MutableStateMap — для мап. Каждый из этих интерфейсов оптимизирован под конкретный сценарий и расширяет базовый MutableState дополнительными методами работы с коллекцией.

SnapshotStateList и SnapshotStateMap — это реализации изменяемых списков и мап, совместимые со снапшотами. Они позволяют отслеживать не просто замену значения, а внутренние изменения: добавление элемента в список, удаление, изменение существующего элемента. Для таких структур mutableStateListOf() и mutableStateMapOf() создают соответствующие наблюдаемые коллекции.

ИнтерфейсНазначениеМетод создания
State<T>Read-only контейнер
MutableState<T>Read-write контейнерmutableStateOf()
MutableIntStateПримитивный Int без boxingmutableIntStateOf()
MutableFloatStateПримитивный Float без boxingmutableFloatStateOf()
SnapshotStateListНаблюдаемый списокmutableStateListOf()
SnapshotStateMapНаблюдаемая мапаmutableStateMapOf()

SnapshotMutationPolicy: когда запускать рекомпозицию

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

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

referentialEquality() — сравнивает ссылки через ===. Рекомпозиция запускается только при присвоении другого объекта, даже если содержимое идентично. Это оптимально для неизменяемых data классов, когда каждый новый экземпляр гарантированно означает изменение. neverEqualPolicy() — всегда считает изменение значимым, не выполняя сравнение. Полезно, когда setter вызывается редко и не нужно тратить время на 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 и аналоги — это специализированные интерфейсы, которые хранят примитивы без автоупаковки (boxing). Обычный MutableState<Int> хранит Int как Integer, что при каждой записи создаёт объект на куче. MutableIntState хранит int (примитив), полностью устраняя overhead упаковки. Это особенно важно при высокочастотных обновлениях — счётчики, позиции скролла, анимационные значения.

mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — функции, создающие примитивные MutableState. Интерфейсы называются MutableIntState, MutableFloatState, MutableLongState. Они расширяют MutableState<Int>, MutableState<Float> и MutableState<Long> соответственно, добавляя свойство intValue как быстрый доступ к примитиву. B их внутренней реализации используется AtomicInteger для lock-free чтения/записи.

Применение: счётчики (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 (read-only) + лямбду onValueChange. Это реализует паттерн State Hoisting и делает компонент переиспользуемым. Компоненты, принимающие MutableState, нарушают однонаправленный поток данных.

Как создать кастомную реализацию MutableState?

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

Итоги

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

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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