mutableStateOf — это функция в Jetpack Compose, которая создаёт контейнер изменяемого наблюдаемого состояния. Когда значение внутри этого контейнера меняется, Compose автоматически запускает рекомпозицию всех компонентов, читающих это состояние. Без mutableStateOf UI не мог бы реактивно обновляться при изменениях данных. По данным Google Android Developers, 2026, mutableStateOf является основным строительным блоком для локального состояния в Compose.
Главное
mutableStateOf — это функция из пакета compose.runtime, которая создаёт объект MutableState
MutableState — это интерфейс с единственным свойством value: геттер для чтения и сеттер для записи. При вызове сеттера Compose Runtime фиксирует изменение в снапшоте и помечает все Composable-функции, читающие эту State-переменную, как требующие рекомпозиции. Этот процесс происходит синхронно в рамках одного snapshot-цикла, что исключает промежуточные состояния при каскадных изменениях.
Parameter policy — второй аргумент mutableStateOf, определяющий поведение при сравнении. structuralEquality() проверяет equals() — это поведение по умолчанию. referentialEquality() проверяет === (ссылочное равенство). neverEqual() считает каждое присваивание изменением. Выбор policy влияет на то, будет ли запускаться рекомпозиция при присвоении "того же самого" значения.
Самый простой способ объявить наблюдаемое состояние — использовать mutableStateOf с remember. Без remember при каждой рекомпозиции создавался бы новый State, и все предыдущие изменения терялись бы. remember гарантирует, что один и тот же MutableState переживает серию рекомпозиций, пока Composable-функция остаётся в составе композиции.
@Composable
fun Counter() {
// Without delegation: read/write via .value
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
@Composable
fun CounterDelegated() {
// With delegation: var + by = Property Delegation
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
Разница между двумя вариантами — синтаксическая. Property Delegation (by) использует соглашение Kotlin: компилятор генерирует вызовы getValue() и setValue() для чтения и записи. Это эквивалентно прямому обращению к count.value, но выглядит как работа с обычной переменной. Оба варианта функционально идентичны: Compose отслеживает чтение в геттере и запись в сеттере независимо от формы записи.
| Форма | Код | Чтение | Запись |
|---|---|---|---|
| Без делегирования | val count = mutableStateOf(0) | count.value | count.value = n |
| С делегированием | var count by mutableStateOf(0) | count | count = n |
Механизм делегированных свойств Kotlin — это не особенность Compose, а встроенная возможность языка. Любой класс может реализовать операторы getValue(thisRef, property) и setValue(thisRef, property, value), после чего его экземпляр можно использовать с ключевым словом by. MutableState именно так и работает: getValue возвращает текущее значение, а setValue присваивает новое.
Важное различие: val и var. mutableStateOf можно присвоить как val, так и var. В случае val (val count = mutableStateOf(0)) сам объект MutableState неизменяем, но его свойство value можно менять. В случае var (var count by mutableStateOf(0)) делегирование даёт иллюзию работы с примитивом, но на самом деле сеттер вызывает setValue на MutableState. Выбор между val и var — это выбор между явным и неявным доступом к .value.
State Delegation — это синтаксический сахар, который упрощает код, но не меняет механику. Компилятор Kotlin транслирует var x by mutableStateOf(0) в getter/setter, которые вызывают mutableStateOf.getValue() и mutableStateOf.setValue(). В сгенерированном байткоде разницы между val и var с by нет — оба работают через один и тот же MutableState-контейнер.
// Custom delegate for Compose State
class ValidatedState<T>(initialValue: T) {
private val state = mutableStateOf(initialValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (value != state.value) {
state.value = value
}
}
}
@Composable
fun Test() {
var text by remember { ValidatedState("") }
}
Snapshot — это механизм Compose Runtime, обеспечивающий консистентность чтения State при параллельных изменениях. Когда Composable-функция читает mutableStateOf, снимок фиксирует текущее значение. Если во время композиции другое изменение записывает в тот же State, снимок видит запись, но не позволяет читать несогласованные данные — чтение всегда возвращает значение, актуальное на момент начала снимка.
При вызове сеттера mutableStateOf.value = newValue Compose Runtime не сразу запускает рекомпозицию. Вместо этого изменение регистрируется в текущем снапшоте. Когда снапшот применяется (на границе фрейма), Compose проходит по списку изменённых State и помечает читающие компоненты как Invalid. Только в следующем фрейме запускается рекомпозиция. Это гарантирует, что UI не перерисовывается десятки раз при каскадных изменениях.
Глобальные и локальные снапшоты: по умолчанию mutableStateOf работает в глобальном снапшоте, который применяется автоматически. Можно создать локальный снапшот через Snapshot.takeSnapshot() для изолированного чтения без побочных эффектов. Это используется внутри Modifier, где нужно прочитать State, но не подписываться на изменения. Такой подход оптимизирует производительность и предотвращает неожиданные рекомпозиции.
Рассмотрим реальный сценарий — форма логина с тремя полями: email, password и статус загрузки. Все три поля используют mutableStateOf, но с разными policy и разной вложенностью. email использует делегирование, password — прямой доступ.
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun LoginForm(onLogin: (String, String) -> Unit) {
// Single State for form, policy = referentialEquality
var formState by remember {
mutableStateOf(LoginState(), SnapshotMutationPolicy.referentialEquality())
}
val isValid = remember(formState) {
formState.email.contains("@") && formState.password.length() >= 6
}
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = formState.email,
onValueChange = { formState = formState.copy(email = it) },
label = { Text("Email") }
)
OutlinedTextField(
value = formState.password,
onValueChange = { formState = formState.copy(password = it) },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = { onLogin(formState.email, formState.password) },
enabled = isValid
) {
Text("Login")
}
}
}
В этом примере mutableStateOf используется с кастомной data-классом LoginState и policy referentialEquality. Это значит, что рекомпозиция запустится только при присвоении нового экземпляра LoginState через copy(). isValid вычисляется на основе formState и пересчитывается только при его изменении. Такой подход даёт чёткий контроль над рекомпозициями: каждое поле формы меняется только через создание новой копии.
Часто задаваемые вопросы
mutableStateOf — это Compose-специфичный контейнер, работающий внутри снапшотов. StateFlow — из kotlinx.coroutines.flow, который не привязан к Compose. mutableStateOf автоматически запускает рекомпозицию, StateFlow требует collectAsState(). Для UI-состояния внутри Composable предпочтительнее mutableStateOf.
Да, mutableStateOf можно вызывать вне Composable-функции, но он не будет отслеживаться. Для работы реактивности в UI нужно читать State внутри Composable. Многие ViewModel используют MutableStateField (обёртку над mutableStateOf) для передачи состояния в UI через StateFlow.
Snapshot система гарантирует консистентность: каждая рекомпозиция видит согласованное состояние на момент начала снимка. Изменения из разных потоков применяются атомарно на границе фрейма, что исключает race condition при чтении внутри одной композиции.
Присвойте новое значение: count.value = 0 (или count = 0 при делегировании). Если нужно полное пересоздание State — используйте remember с ключом: remember(key) { mutableStateOf(initial) } — при изменении key State будет создан заново.
Композер использует снапшоты, которые группируют изменения: даже при сотне присваиваний в одном фрейме рекомпозиция выполняется только один раз. Для сверхчастых обновлений (анимации) используйте Animatable или animate*AsState — они оптимизированы для кадровых обновлений.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также