mutableStateOf — to funkcja w Jetpack Compose, która tworzy kontener zmiennego obserwowalnego stanu. Gdy wartość wewnątrz tego kontenera się zmienia, Compose automatycznie uruchamia rekompozycję wszystkich komponentów odczytujących ten stan. Bez mutableStateOf UI nie mógłby reaktywnie aktualizować się przy zmianach danych. Według Google Android Developers, 2026, mutableStateOf jest podstawowym blokiem konstrukcyjnym dla stanu lokalnego w Compose.
Najważniejsze
mutableStateOf — to funkcja z pakietu compose.runtime, która tworzy obiekt MutableState<T> przechowujący wartość i mogący powiadamiać Compose Runtime o zmianach. Sygnatura: fun <T> mutableStateOf(value: T, policy: SnapshotMutationPolicy<T> = structuralEquality()): MutableState<T>. Parametr policy określa, kiedy zmiana jest uznawana za znaczącą — przy równości strukturalnej, przy równości referencyjnej lub nigdy.
MutableState — to interfejs z pojedynczą właściwością value: getter do odczytu i setter do zapisu. Przy wywołaniu settera Compose Runtime rejestruje zmianę w snapshocie i oznacza wszystkie funkcje Composable odczytujące tę zmienną State jako wymagające rekompozycji. Proces ten odbywa się synchronicznie w ramach jednego cyklu snapshot, co eliminuje stany pośrednie przy kaskadowych zmianach.
Parameter policy — drugi argument mutableStateOf, określający zachowanie przy porównaniu. structuralEquality() sprawdza equals() — to zachowanie domyślne. referentialEquality() sprawdza === (równość referencyjna). neverEqual() uznaje każde przypisanie za zmianę. Wybór policy wpływa na to, czy rekompozycja zostanie uruchomiona przy przypisaniu „tej samej” wartości.
Najprostszy sposób zadeklarowania obserwowalnego stanu — użycie mutableStateOf z remember. Bez remember przy każdej rekompozycji tworzony byłby nowy State, a wszystkie poprzednie zmiany zostałyby utracone. remember gwarantuje, że ten sam MutableState przetrwa serię rekompozycji, dopóki funkcja Composable pozostaje w składzie kompozycji.
@Composable
fun Counter() {
// Bez delegacji: odczyt/zapis przez .value
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
@Composable
fun CounterDelegated() {
// Z delegacją: var + by = Property Delegation
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
Różnica między dwoma wariantami jest składniowa. Property Delegation (by) wykorzystuje konwencję Kotlin: kompilator generuje wywołania getValue() i setValue() do odczytu i zapisu. Jest to równoważne bezpośredniemu odwołaniu do count.value, ale wygląda jak praca ze zwykłą zmienną. Oba warianty są funkcjonalnie identyczne: Compose śledzi odczyt w getterze i zapis w setterze niezależnie od formy zapisu.
| Forma | Kod | Odczyt | Zapis |
|---|---|---|---|
| Bez delegacji | val count = mutableStateOf(0) | count.value | count.value = n |
| Z delegacją | var count by mutableStateOf(0) | count | count = n |
Mechanizm delegowanych właściwości Kotlin — to nie cecha Compose, a wbudowana możliwość języka. Każda klasa może zaimplementować operatory getValue(thisRef, property) i setValue(thisRef, property, value), po czym jej instancja może być używana ze słowem kluczowym by. MutableState właśnie tak działa: getValue zwraca bieżącą wartość, a setValue przypisuje nową.
Ważna różnica: val i var. mutableStateOf można przypisać zarówno do val, jak i var. W przypadku val (val count = mutableStateOf(0)) sam obiekt MutableState jest niezmienny, ale jego właściwość value może być zmieniana. W przypadku var (var count by mutableStateOf(0)) delegacja daje złudzenie pracy z prymitywem, ale w rzeczywistości setter wywołuje setValue na MutableState. Wybór między val a var to wybór między jawnym a niejawnym dostępem do .value.
State Delegation — to syntaktyczny cukier, który upraszcza kod, ale nie zmienia mechaniki. Kompilator Kotlin transluje var x by mutableStateOf(0) na getter/setter, które wywołują mutableStateOf.getValue() i mutableStateOf.setValue(). W wygenerowanym bajtkodzie nie ma różnicy między val a var z by — oba działają przez ten sam kontener MutableState.
// Niestandardowa delegacja dla stanu Compose
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 — to mechanizm Compose Runtime zapewniający spójność odczytu State przy równoległych zmianach. Gdy funkcja Composable odczytuje mutableStateOf, migawka rejestruje bieżącą wartość. Jeśli podczas kompozycji inna zmiana zapisuje do tego samego State, migawka widzi zapis, ale nie pozwala na odczyt niespójnych danych — odczyt zawsze zwraca wartość aktualną na moment rozpoczęcia migawki.
Przy wywołaniu settera mutableStateOf.value = newValue Compose Runtime nie uruchamia natychmiast rekompozycji. Zamiast tego zmiana jest rejestrowana w bieżącym snapshocie. Gdy snapshot jest stosowany (na granicy ramki), Compose przechodzi przez listę zmienionych State i oznacza czytające komponenty jako Invalid. Dopiero w następnej ramce uruchamiana jest rekompozycja. To gwarantuje, że UI nie jest przerysowywany dziesiątki razy przy kaskadowych zmianach.
Globalne i lokalne snapshoty: domyślnie mutableStateOf działa w globalnym snapshocie, który jest stosowany automatycznie. Można utworzyć lokalny snapshot przez Snapshot.takeSnapshot() dla izolowanego odczytu bez efektów ubocznych. Jest to używane wewnątrz Modifier, gdzie trzeba odczytać State, ale nie subskrybować się na zmiany. Takie podejście optymalizuje wydajność i zapobiega nieoczekiwanym rekompozycjom.
Rozważmy rzeczywisty scenariusz — formularz logowania z trzema polami: email, hasło i status ładowania. Wszystkie trzy pola używają mutableStateOf, ale z różnymi policy i różnym zagnieżdżeniem. email używa delegacji, hasło — bezpośredniego dostępu.
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun LoginForm(onLogin: (String, String) -> Unit) {
// Pojedynczy stan dla formularza, polityka = 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("Hasło") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = { onLogin(formState.email, formState.password) },
enabled = isValid
) {
Text("Zaloguj się")
}
}
}
W tym przykładzie mutableStateOf jest używane z niestandardową klasą danych LoginState i policy referentialEquality. Oznacza to, że rekompozycja zostanie uruchomiona tylko przy przypisaniu nowej instancji LoginState przez copy(). isValid jest obliczane na podstawie formState i przeliczane tylko przy jego zmianie. Takie podejście daje wyraźną kontrolę nad rekompozycjami: każde pole formularza zmienia się tylko przez utworzenie nowej kopii.
Często zadawane pytania
mutableStateOf — to kontener specyficzny dla Compose, działający wewnątrz snapshotów. StateFlow pochodzi z kotlinx.coroutines.flow i nie jest związany z Compose. mutableStateOf automatycznie uruchamia rekompozycję, StateFlow wymaga collectAsState(). Dla stanu UI wewnątrz Composable preferowany jest mutableStateOf.
Tak, mutableStateOf można wywoływać poza funkcją Composable, ale nie będzie śledzony. Do działania reaktywności w UI należy czytać State wewnątrz Composable. Wiele ViewModel używa MutableStateField (opakowania na mutableStateOf) do przekazywania stanu do UI przez StateFlow.
Snapshot system gwarantuje spójność: każda rekompozycja widzi spójny stan na moment rozpoczęcia migawki. Zmiany z różnych wątków są stosowane atomowo na granicy ramki, co eliminuje race condition przy odczycie w ramach jednej kompozycji.
Przypisz nową wartość: count.value = 0 (lub count = 0 przy delegacji). Jeśli potrzebne jest całkowite odtworzenie State — użyj remember z kluczem: remember(key) { mutableStateOf(initial) } — przy zmianie key State zostanie utworzony od nowa.
Composer używa snapshotów, które grupują zmiany: nawet przy stu przypisaniach w jednej ramce rekompozycja wykonywana jest tylko raz. Dla bardzo częstych aktualizacji (animacje) użyj Animatable lub animate*AsState — są zoptymalizowane do odświeżania klatek.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również