MutableState — to interfejs w Jetpack Compose, który reprezentuje kontener dla zmiennej obserwowalnej wartości. Stanowi podstawę reaktywnego systemu Compose: za każdym razem, gdy wartość MutableState zmienia się przez setter, Compose Runtime powiadamia wszystkie czytające komponenty i uruchamia rekombinację. Według Google Android Developers, 2026, zrozumienie MutableState jest obowiązkowe do prawidłowej pracy ze stanem w deklaratywnym UI.
Najważniejsze
MutableState — to interfejs z pakietu androidx.compose.runtime, deklarujący jedną właściwość: override var value: T. Getter zwraca bieżącą wartość, setter zapisuje nową i powiadamia Compose Runtime o zmianie. Interfejs dziedziczy po State<T>, w którym value jest dostępny tylko do odczytu. Taka dwupoziomowa architektura pozwala rozdzielić dostęp: komponent, który potrzebuje tylko odczytać wartość, otrzymuje State<T>, a komponent-właściciel — MutableState<T>.
Domyślną implementacją MutableState jest wewnętrzna klasa SnapshotMutableStateImpl, która wykorzystuje mechanizm snapshotów do śledzenia zmian. Gdy setter value jest wywoływany, bieżący snapshot rejestruje zapis i oznacza wszystkie zarejestrowane ObservedScope (obszary obserwacji) jako nieprawidłowe. Obszary te (zwykle funkcje Composable) zostaną przekształcone w następnej klatce. Cały proces odbywa się synchronicznie i bez blokad dzięki architekturze Lock-free snapshot.
State vs MutableState: State — to interfejs tylko do odczytu, używany do publicznych API komponentów. Gdy deklarujesz parametr funkcji Composable jako State<Int>, gwarantujesz, że komponent może czytać, ale nie zmieniać stanu. MutableState jest używany wewnątrz komponentu-właściciela. Takie rozdzielenie to jedna z podstawowych praktyk Compose, zapobiegająca nieautoryzowanym zmianom.
Hierarchia interfejsów stanów w Compose ma kilka poziomów. Na szczycie znajduje się State<T> z value tylko do odczytu. Poniżej — MutableState<T> z value do odczytu i zapisu. Dalej idą wyspecjalizowane prymitywne wersje: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState i inne, które unikają autopakowania (boxing) prymitywów.
MutableDoubleState i MutableLongState — mniej popularne, ale również istniejące typy. Interfejsy kolekcji: MutableListState — do śledzenia zmian wewnątrz listy, MutableStateMap — dla map. Każdy z tych interfejsów jest zoptymalizowany pod konkretny scenariusz i rozszerza bazowy MutableState o dodatkowe metody pracy z kolekcją.
SnapshotStateList i SnapshotStateMap — to implementacje modyfikowalnych list i map, kompatybilne ze snapshotami. Pozwalają śledzić nie tylko zastąpienie wartości, ale także wewnętrzne zmiany: dodanie elementu do listy, usunięcie, modyfikację istniejącego elementu. Dla takich struktur mutableStateListOf() i mutableStateMapOf() tworzą odpowiednie obserwowalne kolekcje.
| Interfejs | Przeznaczenie | Metoda tworzenia |
|---|---|---|
| State<T> | Kontener tylko do odczytu | — |
| MutableState<T> | Kontener do odczytu i zapisu | mutableStateOf() |
| MutableIntState | Prymitywny Int bez boxingu | mutableIntStateOf() |
| MutableFloatState | Prymitywny Float bez boxingu | mutableFloatStateOf() |
| SnapshotStateList | Obserwowalna lista | mutableStateListOf() |
| SnapshotStateMap | Obserwowalna mapa | mutableStateMapOf() |
SnapshotMutationPolicy — to interfejs określający, kiedy zmiana MutableState jest uznawana za znaczącą. mutableStateOf przyjmuje policy jako drugi argument. Standardowe implementacje: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (zawsze uznaje zmianę). Dla własnej logiki można zaimplementować własny policy.
structuralEquality() — domyślne zachowanie. Compose porównuje nową wartość ze starą przez equals(). Jeśli wynik to true — rekombinacja NIE jest uruchamiana. Jest to wygodne dla prymitywów i data classes, gdzie dwa egzemplarze z tymi samymi polami są uznawane za równe. Problem: jeśli data class zawiera List, equals() wykonuje głębokie porównanie, co może być kosztowne dla dużych list.
referentialEquality() — porównuje referencje przez ===. Rekombinacja uruchamiana tylko przy przypisaniu innego obiektu, nawet jeśli zawartość jest identyczna. Jest to optymalne dla niemutowalnych data classes, gdy każdy nowy egzemplarz gwarantuje zmianę. neverEqualPolicy() — zawsze uznaje zmianę za znaczącą, nie wykonując porównania. Przydatne, gdy setter jest wywoływany rzadko i nie trzeba tracić czasu na equals.
// Porównanie polityk w praktyce
data class User(val name: String, val age: Int)
@Composable
fun UserProfile() {
// structuralEquality: rekombinacja TYLKO jeśli dane się zmieniły
var user1 by remember {
mutableStateOf(User("Alice", 30))
}
// referentialEquality: rekombinacja przy KAŻDYM przypisaniu
var user2 by remember {
mutableStateOf(User("Bob", 25),
SnapshotMutationPolicy.referentialEquality())
}
// user1: copy() z tymi samymi polami NIE wywołuje rekombinacji
// user2: nawet user2.copy() == user2 wywołuje rekombinację (nowa ref)
}
Prymitywne MutableIntState i analogiczne — to wyspecjalizowane interfejsy, które przechowują prymitywy bez autopakowania (boxing). Zwykły MutableState<Int> przechowuje Int jako Integer, co przy każdym zapisie tworzy obiekt na stercie. MutableIntState przechowuje int (prymityw), całkowicie eliminując narzut pakowania. Jest to szczególnie ważne przy wysokoczęstotliwościowych aktualizacjach — liczniki, pozycje scrolla, wartości animacyjne.
mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — funkcje tworzące prymitywne MutableState. Interfejsy nazywają się MutableIntState, MutableFloatState, MutableLongState. Rozszerzają one MutableState<Int>, MutableState<Float> i MutableState<Long> odpowiednio, dodając właściwość intValue jako szybki dostęp do prymitywu. W ich wewnętrznej implementacji używany jest AtomicInteger do lock-free odczytu/zapisu.
Zastosowanie: liczniki (Int), pozycje scrolla (Float offset), znaczniki czasu (Long). W większości codziennych scenariuszy różnica w wydajności jest niezauważalna, ale w LazyList z tysiącami elementów i animacją przejść prymitywne State dają odczuwalny wzrost. Google zaleca używanie prymitywnych State dla typowych scenariuszy zamiast uniwersalnego mutableStateOf.
@Composable
fun ScrollCounter() {
// Źle: autopakowanie przy każdej aktualizacji
var badCount by remember { mutableStateOf(0) }
// Dobrze: brak autopakowania, prymitywne przechowywanie
var goodCount by remember { mutableIntStateOf(0) }
// Użycie jest identyczne
Button(onClick = { goodCount++ }) {
Text("Count: $goodCount")
}
}
Rozważmy komponent TodoList, gdzie MutableState jest używany w dwóch formach: jako oddzielne zmienne dla stanu wejścia i jako SnapshotStateList dla dynamicznej listy zadań. Oba wykorzystują delegację dla zwięzłości kodu.
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("Dodaj") }
}
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 tworzy SnapshotStateList — modyfikowalną listę, która śledzi zmiany poszczególnych elementów. Przy wywołaniu items.add() i items[n] = newValue Compose widzi mutację i przeprowadza rekombinację tylko tych elementów LazyColumn, które się zmieniły. inputText — zwykły MutableState<String>. Kombinacja dwóch typów MutableState (pojedynczy i kolekcja) — typowy wzorzec dla ekranów z formularzami i listami.
Często zadawane pytania
MutableState bez remember będzie tworzony na nowo przy każdej rekombinacji. Każde nowe wywołanie mutableStateOf tworzy nowy obiekt, a stara wartość zostaje utracona. Zawsze używaj remember do zachowania State między rekombinacjami, chyba że State jest tworzony poza Composable (np. w ViewModel).
Przeczytaj .value jeden raz poza snapshotem przez snapshot { }. Ale to wyłącza reaktywność — zmiany nie będą już wywoływać rekombinacji. Do jednorazowego odczytu bez subskrypcji użyj currentValue() wewnątrz snapshot bez odczytu.
mutableIntStateOf jest szybsze, ponieważ nie wymaga pakowania int w Integer. Przy tysiącach aktualizacji na sekundę (animacja, scroll) różnica może sięgać 30-50% czasu alokacji. Przy rzadkich aktualizacjach (kliknięcia, wprowadzanie tekstu) różnica jest nieznaczna.
Można, ale nie jest zalecane. Zamiast MutableState przekazuj State (tylko do odczytu) + lambdę onValueChange. To realizuje wzorzec State Hoisting i czyni komponent wielokrotnego użytku. Komponenty przyjmujące MutableState naruszają jednokierunkowy przepływ danych.
Zaimplementuj interfejs MutableState i dostarcz override var value z getterem i setterem. W setterze można dodać walidację lub logowanie. Dla wstecznej kompatybilności z Compose Runtime opakuj swoją implementację w snapshotFlow lub użyj snapshotIncrement.
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ż