MutableState è un'interfaccia in Jetpack Compose che rappresenta un contenitore per un valore mutabile osservabile. È il fondamento del sistema reattivo di Compose: ogni volta che il valore di MutableState cambia tramite il setter, Compose Runtime notifica tutti i componenti lettori e attiva la ricomposizione. Secondo Google Android Developers, 2026, comprendere MutableState è essenziale per lavorare correttamente con lo stato in una UI dichiarativa.
Punti chiave
MutableState è un'interfaccia del pacchetto androidx.compose.runtime che dichiara una singola proprietà: override var value: T. Il getter restituisce il valore corrente, il setter ne scrive uno nuovo e notifica Compose Runtime della modifica. L'interfaccia eredita da State<T>, dove value è in sola lettura. Questa architettura a due livelli consente di separare l'accesso: un componente che deve solo leggere il valore riceve State<T>, mentre il componente proprietario riceve MutableState<T>.
L'implementazione predefinita di MutableState è la classe interna SnapshotMutableStateImpl, che utilizza un meccanismo di snapshot per tracciare le modifiche. Quando viene chiamato il setter di value, lo snapshot corrente registra la scrittura e segna tutti gli ObservedScope registrati come non validi. Questi ambiti (di solito funzioni Composable) verranno ricomposti nel fotogramma successivo. L'intero processo avviene in modo sincrono e senza blocchi grazie all'architettura Lock-free snapshot.
State vs MutableState: State è un'interfaccia di sola lettura utilizzata per le API pubbliche dei componenti. Quando dichiari un parametro di funzione Composable come State<Int>, garantisci che il componente possa leggere ma non modificare lo stato. MutableState viene utilizzato all'interno del componente proprietario. Questa separazione è una delle pratiche di base di Compose che previene modifiche non autorizzate.
La gerarchia delle interfacce di stato in Compose ha diversi livelli. Al vertice c'è State<T> con value in sola lettura. Sotto c'è MutableState<T> con value in lettura-scrittura. Più in basso vengono le versioni primitive specializzate: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState e altre, che evitano il boxing dei primitivi.
MutableDoubleState e MutableLongState sono meno comuni ma esistono anch'essi. Interfacce di collezioni: MutableListState — per tracciare modifiche all'interno di una lista, MutableStateMap — per mappe. Ognuna di queste interfacce è ottimizzata per uno scenario specifico ed estende la MutableState di base con metodi aggiuntivi di manipolazione delle collezioni.
SnapshotStateList e SnapshotStateMap sono implementazioni di liste e mappe mutabili compatibili con snapshot. Consentono di tracciare non solo la sostituzione del valore, ma le modifiche interne: aggiunta di un elemento a una lista, rimozione, modifica di un elemento esistente. Per tali strutture, mutableStateListOf() e mutableStateMapOf() creano le corrispondenti collezioni osservabili.
| Interfaccia | Scopo | Metodo di creazione |
|---|---|---|
| State<T> | Contenitore di sola lettura | — |
| MutableState<T> | Contenitore lettura-scrittura | mutableStateOf() |
| MutableIntState | Int primitivo senza boxing | mutableIntStateOf() |
| MutableFloatState | Float primitivo senza boxing | mutableFloatStateOf() |
| SnapshotStateList | Lista osservabile | mutableStateListOf() |
| SnapshotStateMap | Mappa osservabile | mutableStateMapOf() |
SnapshotMutationPolicy è un'interfaccia che determina quando una modifica in MutableState è considerata significativa. mutableStateOf accetta policy come secondo argomento. Implementazioni standard: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (considera sempre la modifica significativa). È possibile implementare una politica personalizzata per logica propria.
structuralEquality() — comportamento predefinito. Compose confronta il nuovo valore con il vecchio tramite equals(). Se il risultato è true, la ricomposizione NON viene attivata. Ciò è comodo per primitivi e data class, dove due istanze con gli stessi campi sono considerate uguali. Problema: se una data class contiene una List, equals() esegue un confronto profondo, che può essere costoso per liste grandi.
referentialEquality() — confronta i riferimenti tramite ===. La ricomposizione viene attivata solo quando viene assegnato un oggetto diverso, anche se il contenuto è identico. È ottimale per data class immutabili dove ogni nuova istanza garantisce una modifica. neverEqualPolicy() — considera sempre la modifica significativa senza eseguire il confronto. Utile quando il setter viene chiamato raramente e non c'è bisogno di spendere tempo in equals.
// Policy comparison in practice
data class User(val name: String, val age: Int)
@Composable
fun UserProfile() {
// structuralEquality: recomposition ONLY if data changed
var user1 by remember {
mutableStateOf(User("Alice", 30))
}
// referentialEquality: recomposition on ANY assignment
var user2 by remember {
mutableStateOf(User("Bob", 25),
SnapshotMutationPolicy.referentialEquality())
}
// user1: copy() with same fields does NOT trigger recomposition
// user2: even user2.copy() == user2 triggers recomposition (new ref)
}
MutableIntState primitivo e simili sono interfacce specializzate che memorizzano primitivi senza boxing. Un normale MutableState<Int> memorizza Int come Integer, che crea un oggetto sull'heap a ogni scrittura. MutableIntState memorizza int (primitivo), eliminando completamente l'overhead del boxing. Ciò è particolarmente importante per aggiornamenti ad alta frequenza — contatori, posizioni di scorrimento, valori di animazione.
mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — funzioni che creano MutableState primitivi. Le interfacce si chiamano MutableIntState, MutableFloatState, MutableLongState. Estendono rispettivamente MutableState<Int>, MutableState<Float> e MutableState<Long>, aggiungendo la proprietà intValue per l'accesso rapido al primitivo. La loro implementazione interna utilizza AtomicInteger per lettura/scrittura senza blocco.
Utilizzo: contatori (Int), posizioni di scorrimento (Float offset), timestamp (Long). Nella maggior parte degli scenari quotidiani la differenza di prestazioni è impercettibile, ma in LazyList con migliaia di elementi e animazioni di transizione, gli State primitivi forniscono un miglioramento notevole. Google raccomanda di utilizzare State primitivi per scenari tipici invece del mutableStateOf universale.
@Composable
fun ScrollCounter() {
// Bad: boxing on every update
var badCount by remember { mutableStateOf(0) }
// Good: no boxing, primitive storage
var goodCount by remember { mutableIntStateOf(0) }
// Usage is identical
Button(onClick = { goodCount++ }) {
Text("Count: $goodCount")
}
}
Consideriamo un componente TodoList, dove MutableState viene utilizzato in due forme: come variabili separate per lo stato di input e come SnapshotStateList per una lista dinamica di attività. Entrambi utilizzano la delega per la brevità del codice.
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("Add") }
}
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 crea un SnapshotStateList — una lista mutabile che traccia le modifiche ai singoli elementi. Quando vengono chiamati items.add() e items[n] = newValue, Compose vede la mutazione e ricompone solo gli elementi di LazyColumn che sono cambiati. inputText è un normale MutableState<String>. La combinazione di due tipi di MutableState (singolo e collezione) è un pattern tipico per schermate con moduli e liste.
Domande frequenti
MutableState senza remember verrà creato nuovamente a ogni ricomposizione. Ogni nuova chiamata a mutableStateOf crea un nuovo oggetto e il vecchio valore viene perso. Usa sempre remember per preservare lo State tra le ricomposizioni, a meno che lo State non venga creato all'esterno di un Composable (ad esempio, in un ViewModel).
Leggi .value una volta al di fuori di uno snapshot tramite snapshot { }. Ma questo disabilita la reattività — le modifiche non attiveranno più la ricomposizione. Per una lettura singola senza sottoscrizione, usa currentValue() all'interno di uno snapshot senza lettura.
mutableIntStateOf è più veloce perché non richiede il boxing di int in Integer. Con migliaia di aggiornamenti al secondo (animazione, scorrimento), la differenza può raggiungere il 30-50% nel tempo di allocazione. Per aggiornamenti rari (clic, immissione di testo), la differenza è trascurabile.
È possibile ma non raccomandato. Invece di MutableState, passa State (sola lettura) + un lambda onValueChange. Questo implementa il pattern State Hoisting e rende il componente riutilizzabile. I componenti che accettano MutableState violano il flusso di dati unidirezionale.
Implementa l'interfaccia MutableState e fornisci override var value con un getter e setter. Nel setter puoi aggiungere validazione o registrazione. Per la retrocompatibilità con Compose Runtime, avvolgi la tua implementazione personalizzata in snapshotFlow o usa snapshotIncrement.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche