MutableState — obserwowany stan i mechanizm aktualizacji w Compose

Autor: IT Sectr Opublikowano: 2026-06-28 Czas czytania: 7 min

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 — interfejs compose.runtime z jedną właściwością value (getter + setter)
  • State — nadrzędny interfejs tylko do odczytu, MutableState dodaje możliwość zapisu
  • Rekombinacja uruchamiana przy wywołaniu settera value wewnątrz aktywnego cyklu snapshot
  • SnapshotMutationPolicy określa, kiedy zmiana jest uznawana za znaczącą dla rekombinacji
  • MutableIntState i analogiczne — zoptymalizowane prymitywne wersje MutableState

Czym jest MutableState w Jetpack Compose

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 State, MutableState i pochodnych interfejsów

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.

InterfejsPrzeznaczenieMetoda tworzenia
State<T>Kontener tylko do odczytu
MutableState<T>Kontener do odczytu i zapisumutableStateOf()
MutableIntStatePrymitywny Int bez boxingumutableIntStateOf()
MutableFloatStatePrymitywny Float bez boxingumutableFloatStateOf()
SnapshotStateListObserwowalna listamutableStateListOf()
SnapshotStateMapObserwowalna mapamutableStateMapOf()

SnapshotMutationPolicy: kiedy uruchomić rekombinację

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.

kotlin
    // 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 State: MutableIntState, MutableFloatState, MutableLongState

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.

kotlin
@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")
    }
}

Praktyczne przykłady pracy z MutableState

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.

kotlin
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

Czy można używać MutableState bez remember?

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).

Jak przekształcić MutableState w zwykłą zmienną?

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.

Co jest szybsze: mutableStateOf czy mutableIntStateOf?

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.

Czy można używać MutableState jako argumentu funkcji Composable?

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.

Jak stworzyć własną implementację MutableState?

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

  • MutableState — podstawowy interfejs dla zmiennego obserwowalnego stanu w Compose
  • State — wersja tylko do odczytu do przekazywania danych bez prawa zmiany
  • SnapshotMutationPolicy zarządza warunkami uruchamiania rekombinacji przy zmianie
  • Prymitywne State (MutableIntState i inne) eliminują narzut autopakowania
  • SnapshotStateList i SnapshotStateMap śledzą wewnętrzne zmiany kolekcji
  • Rekombinacja uruchamiana automatycznie przy każdym wywołaniu settera value
  • Zalecenie: używaj MutableState do posiadania stanu i State do przekazywania w dół

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.

Omów projekt

Przeczytaj również