MutableState — е интерфейс в Jetpack Compose, който представлява контейнер за променяема наблюдаема стойност. Той е основата на реактивната система на Compose: всеки път, когато стойността на MutableState се променя чрез setter, Compose Runtime уведомява всички четящи компоненти и стартира рекомпозиция. Според Google Android Developers, 2026, разбирането на MutableState е задължително за коректна работа със състояние в декларативния UI.
Основни точки
MutableState — е интерфейс от пакета androidx.compose.runtime, който декларира едно свойство: override var value: T. Getter връща текущата стойност, setter записва нова и уведомява Compose Runtime за промяната. Интерфейсът наследява от State<T>, при който value е достъпен само за четене. Такава двустепенна архитектура позволява разделяне на достъпа: компонентът, който трябва само да чете стойността, получава State<T>, а компонентът-собственик — MutableState<T>.
Имплементацията по подразбиране на MutableState е вътрешният клас SnapshotMutableStateImpl, който използва механизма на snapshot за проследяване на промени. Когато setter на value бъде извикан, текущият snapshot записва записа и маркира всички регистрирани ObservedScope (области на наблюдение) като невалидни. Тези области (обикновено Composable функции) ще бъдат рекомпозирани в следващия кадър. Целият процес се случва синхронно и без блокиране благодарение на архитектурата Lock-free snapshot.
State срещу MutableState: State — е интерфейс само за четене, използван за публичните API на компонентите. Когато декларирате параметър на Composable функция като State<Int>, гарантирате, че компонентът може да чете, но не и да променя състоянието. MutableState се използва вътре в компонента-собственик. Такова разделение — една от основните практики на Compose — предотвратява неоторизирани промени.
Йерархията на интерфейсите за състояние в Compose има няколко нива. На върха — State<T> с value само за четене. По-долу — MutableState<T> с value за четене и запис. Следват специализирани примитивни версии: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState и други, които избягват автоматичното опаковане (boxing) на примитиви.
MutableDoubleState и MutableLongState — по-рядко срещани, но съществуващи типове. Интерфейси за колекции: MutableListState — за проследяване на промени вътре в списък, MutableStateMap — за карти. Всеки от тези интерфейси е оптимизиран за конкретен сценарий и разширява базовия MutableState с допълнителни методи за работа с колекция.
SnapshotStateList и SnapshotStateMap — са имплементации на променяеми списъци и карти, съвместими със snapshot. Те позволяват проследяване не само на замяна на стойност, но и на вътрешни промени: добавяне на елемент в списък, изтриване, промяна на съществуващ елемент. За такива структури mutableStateListOf() и mutableStateMapOf() създават съответните наблюдаеми колекции.
| Интерфейс | Предназначение | Метод за създаване |
|---|---|---|
| State<T> | Контейнер само за четене | — |
| MutableState<T> | Контейнер за четене и запис | 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 клас съдържа List, equals() извършва дълбоко сравнение, което може да бъде скъпо за големи списъци.
referentialEquality() — сравнява референции чрез ===. Рекомпозиция се стартира само при присвояване на друг обект, дори ако съдържанието е идентично. Това е оптимално за неизменяеми data класове, където всеки нов инстанс гарантирано означава промяна. neverEqualPolicy() — винаги счита промяната за значима, без да извършва сравнение. Полезно, когато setter се извиква рядко и не е необходимо да се губи време за equals.
// Сравнение на политики на практика
data class User(val name: String, val age: Int)
@Composable
fun UserProfile() {
// structuralEquality: рекомпозиция САМО ако данните са се променили
var user1 by remember {
mutableStateOf(User("Alice", 30))
}
// referentialEquality: рекомпозиция при ВСЯКО присвояване
var user2 by remember {
mutableStateOf(User("Bob", 25),
SnapshotMutationPolicy.referentialEquality())
}
// user1: copy() със същите полета НЕ задейства рекомпозиция
// user2: дори user2.copy() == user2 задейства рекомпозиция (нова референция)
}
Примитивните MutableIntState и аналогични — са специализирани интерфейси, които съхраняват примитиви без автоматично опаковане (boxing). Обикновеният MutableState<Int> съхранява Int като Integer, което при всеки запис създава обект в heap. MutableIntState съхранява int (примитив), напълно елиминирайки overhead-а на опаковане. Това е особено важно при високочестотни актуализации — броячи, позиции на скрол, анимационни стойности.
mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — функции, които създават примитивни MutableState. Интерфейсите се наричат MutableIntState, MutableFloatState, MutableLongState. Те разширяват съответно MutableState<Int>, MutableState<Float> и MutableState<Long>, добавяйки свойството intValue за бърз достъп до примитива. В тяхната вътрешна имплементация се използва AtomicInteger за lock-free четене/запис.
Приложение: броячи (Int), позиции на скрол (Float offset), времеви отпечатъци (Long). В повечето ежедневни сценарии разликата в производителността е незабележима, но в LazyList с хиляди елементи и анимация на преходи примитивните State дават осезаемо подобрение. Google препоръчва използването на примитивни State за типични сценарии вместо универсалния mutableStateOf.
@Composable
fun ScrollCounter() {
// Лошо: опаковане при всяка актуализация
var badCount by remember { mutableStateOf(0) }
// Добре: без опаковане, примитивно съхранение
var goodCount by remember { mutableIntStateOf(0) }
// Използването е идентично
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("Добави") }
}
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 чрез snapshot { }. Но това изключва реактивността — промените вече няма да предизвикват рекомпозиция. За еднократно четене без абонамент използвайте currentValue() вътре в snapshot без четене.
mutableIntStateOf е по-бързо, тъй като не изисква опаковане на int в Integer. При хиляди актуализации в секунда (анимация, скрол) разликата може да достигне 30-50% от времето за алокация. При редки актуализации (кликвания, въвеждане на текст) разликата е незначителна.
Може, но не се препоръчва. Вместо MutableState предавайте State (само за четене) + ламбда onValueChange. Това имплементира модела State Hoisting и прави компонента преизползваем. Компонентите, които приемат MutableState, нарушават еднопосочния поток от данни.
Имплементирайте интерфейса MutableState и предоставете override var value с getter и setter. В setter можете да добавите валидация или логване. За обратна съвместимост с Compose Runtime увийте имплементацията си в snapshotFlow или използвайте snapshotIncrement.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също