MutableState — это интерфейс в Jetpack Compose, который представляет контейнер для изменяемого наблюдаемого значения. Он является основой реактивной системы Compose: каждый раз, когда значение MutableState меняется через сеттер, Compose Runtime уведомляет все читающие компоненты и запускает рекомпозицию. По данным Google Android Developers, 2026, понимание MutableState является обязательным для корректной работы с состоянием в декларативном UI.
Главное
MutableState — это интерфейс из пакета androidx.compose.runtime, объявляющий одно свойство: override var value: T. Геттер возвращает текущее значение, сеттер записывает новое и уведомляет Compose Runtime об изменении. Интерфейс наследуется от State
Реализацией MutableState по умолчанию является внутренний класс SnapshotMutableStateImpl, который использует механизм снапшотов для отслеживания изменений. Когда сеттер value вызывается, текущий снапшот фиксирует запись и помечает все зарегистрированные ObservedScope (области наблюдения) как невалидные. Эти области (обычно Composable-функции) будут перекомпонованы на следующем кадре. Весь процесс происходит синхронно и без блокировок благодаря архитектуре Lock-free snapshot.
State vs MutableState: State — это интерфейс только для чтения, используемый для публичных API компонентов. Когда вы объявляете параметр Composable-функции как State<Int>, вы гарантируете, что компонент может читать, но не менять состояние. MutableState используется внутри компонента-владельца. Такое разделение — одна из базовых практик Compose, предотвращающая несанкционированные изменения.
Иерархия интерфейсов состояний в 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 без boxing | mutableIntStateOf() |
| MutableFloatState | Примитивный Float без boxing | mutableFloatStateOf() |
| SnapshotStateList | Наблюдаемый список | mutableStateListOf() |
| SnapshotStateMap | Наблюдаемая мапа | mutableStateMapOf() |
SnapshotMutationPolicy — это интерфейс, определяющий, когда изменение MutableState считается значимым. mutableStateOf принимает policy вторым аргументом. Стандартные реализации: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (всегда считает изменение). Для пользовательской логики можно реализовать свой policy.
structuralEquality() — поведение по умолчанию. Compose сравнивает новое значение со старым через equals(). Если результат true — рекомпозиция НЕ запускается. Это удобно для примитивов и data классов, где два экземпляра с одинаковыми полями считаются равными. Проблема: если data class содержит List, equals() выполняет глубокое сравнение, что может быть затратно для больших списков.
referentialEquality() — сравнивает ссылки через ===. Рекомпозиция запускается только при присвоении другого объекта, даже если содержимое идентично. Это оптимально для неизменяемых data классов, когда каждый новый экземпляр гарантированно означает изменение. neverEqualPolicy() — всегда считает изменение значимым, не выполняя сравнение. Полезно, когда setter вызывается редко и не нужно тратить время на 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 и аналоги — это специализированные интерфейсы, которые хранят примитивы без автоупаковки (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.
@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 (read-only) + лямбду onValueChange. Это реализует паттерн State Hoisting и делает компонент переиспользуемым. Компоненты, принимающие MutableState, нарушают однонаправленный поток данных.
Реализуйте интерфейс MutableState и предоставьте override var value с геттером и сеттером. В сеттере можно добавить валидацию или логирование. Для обратной совместимости с Compose Runtime оберните свою реализацию в snapshotFlow или используйте snapshotIncrement.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также