MutableState est une interface dans Jetpack Compose qui représente un conteneur pour une valeur mutable observable. C'est le fondement du système réactif de Compose : chaque fois que la valeur de MutableState change via le setter, Compose Runtime notifie tous les composants lecteurs et déclenche la recomposition. Selon Google Android Developers, 2026, comprendre MutableState est essentiel pour travailler correctement avec l'état dans une UI déclarative.
Points clés
MutableState est une interface du paquetage androidx.compose.runtime qui déclare une seule propriété : override var value: T. Le getter retourne la valeur actuelle, le setter écrit une nouvelle valeur et notifie Compose Runtime du changement. L'interface hérite de State<T>, où value est en lecture seule. Cette architecture à deux niveaux permet de séparer l'accès : un composant qui a seulement besoin de lire la valeur reçoit State<T>, tandis que le composant propriétaire reçoit MutableState<T>.
L'implémentation par défaut de MutableState est la classe interne SnapshotMutableStateImpl, qui utilise un mécanisme de snapshots pour suivre les changements. Lorsque le setter de value est appelé, le snapshot actuel enregistre l'écriture et marque tous les ObservedScope enregistrés comme invalides. Ces portées (généralement des fonctions Composable) seront recomposées dans l'image suivante. Tout le processus se produit de manière synchrone et sans verrou grâce à l'architecture Lock-free snapshot.
State vs MutableState : State est une interface en lecture seule utilisée pour les API publiques des composants. Lorsque vous déclarez un paramètre de fonction Composable comme State<Int>, vous garantissez que le composant peut lire mais ne peut pas modifier l'état. MutableState est utilisé à l'intérieur du composant propriétaire. Cette séparation est l'une des pratiques de base de Compose qui empêche les modifications non autorisées.
La hiérarchie des interfaces d'état dans Compose a plusieurs niveaux. Au sommet se trouve State<T> avec une value en lecture seule. En dessous se trouve MutableState<T> avec une value en lecture-écriture. Plus bas viennent les versions primitives spécialisées : MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState et autres, qui évitent le boxing des primitifs.
MutableDoubleState et MutableLongState sont moins courants mais existent également. Interfaces de collections : MutableListState — pour suivre les changements dans une liste, MutableStateMap — pour les maps. Chacune de ces interfaces est optimisée pour un scénario spécifique et étend la MutableState de base avec des méthodes supplémentaires de manipulation de collections.
SnapshotStateList et SnapshotStateMap sont des implémentations de listes et de maps mutables compatibles avec les snapshots. Ils permettent de suivre non seulement le remplacement de valeur, mais aussi les changements internes : ajout d'un élément à une liste, suppression, modification d'un élément existant. Pour ces structures, mutableStateListOf() et mutableStateMapOf() créent les collections observables correspondantes.
| Interface | Objectif | Méthode de création |
|---|---|---|
| State<T> | Conteneur en lecture seule | — |
| MutableState<T> | Conteneur lecture-écriture | mutableStateOf() |
| MutableIntState | Int primitif sans boxing | mutableIntStateOf() |
| MutableFloatState | Float primitif sans boxing | mutableFloatStateOf() |
| SnapshotStateList | Liste observable | mutableStateListOf() |
| SnapshotStateMap | Map observable | mutableStateMapOf() |
SnapshotMutationPolicy est une interface qui détermine quand un changement dans MutableState est considéré comme significatif. mutableStateOf accepte policy comme deuxième argument. Implémentations standard : structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (considère toujours le changement comme significatif). Une politique personnalisée peut être implémentée pour une logique propre.
structuralEquality() — comportement par défaut. Compose compare la nouvelle valeur avec l'ancienne via equals(). Si le résultat est true, la recomposition N'est PAS déclenchée. C'est pratique pour les primitifs et les data classes, où deux instances avec les mêmes champs sont considérées comme égales. Problème : si une data class contient une List, equals() effectue une comparaison profonde, ce qui peut être coûteux pour les grandes listes.
referentialEquality() — compare les références via ===. La recomposition est déclenchée uniquement lorsqu'un objet différent est assigné, même si le contenu est identique. C'est optimal pour les data classes immuables où chaque nouvelle instance garantit un changement. neverEqualPolicy() — considère toujours le changement comme significatif sans effectuer de comparaison. Utile lorsque le setter est appelé rarement et qu'il n'est pas nécessaire de perdre du temps avec 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 primitif et similaires sont des interfaces spécialisées qui stockent des primitifs sans boxing. Un MutableState<Int> normal stocke Int comme Integer, ce qui crée un objet sur le heap à chaque écriture. MutableIntState stocke int (primitif), éliminant complètement le surcoût du boxing. C'est particulièrement important pour les mises à jour à haute fréquence — compteurs, positions de défilement, valeurs d'animation.
mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — fonctions qui créent des MutableState primitifs. Les interfaces sont appelées MutableIntState, MutableFloatState, MutableLongState. Elles étendent respectivement MutableState<Int>, MutableState<Float> et MutableState<Long>, ajoutant la propriété intValue pour un accès rapide au primitif. Leur implémentation interne utilise AtomicInteger pour la lecture/écriture sans verrou.
Utilisation : compteurs (Int), positions de défilement (Float offset), horodatages (Long). Dans la plupart des scénarios quotidiens, la différence de performance est imperceptible, mais dans LazyList avec des milliers d'éléments et des animations de transition, les State primitifs offrent une amélioration notable. Google recommande d'utiliser les State primitifs pour les scénarios typiques au lieu du mutableStateOf universel.
@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")
}
}
Considérons un composant TodoList, où MutableState est utilisé sous deux formes : comme variables séparées pour l'état d'entrée et comme SnapshotStateList pour une liste dynamique de tâches. Les deux utilisent la délégation pour la concision du code.
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 crée un SnapshotStateList — une liste mutable qui suit les changements des éléments individuels. Lorsque items.add() et items[n] = newValue sont appelés, Compose voit la mutation et recompose uniquement les éléments de LazyColumn qui ont changé. inputText est un MutableState<String> normal. La combinaison de deux types de MutableState (individuel et collection) est un motif typique pour les écrans avec formulaires et listes.
Questions fréquentes
MutableState sans remember sera recréé à chaque recomposition. Chaque nouvel appel à mutableStateOf crée un nouvel objet et l'ancienne valeur est perdue. Utilisez toujours remember pour conserver l'état entre les recompositions, à moins que l'état ne soit créé en dehors d'un Composable (par exemple, dans un ViewModel).
Lisez .value une fois en dehors d'un snapshot via snapshot { }. Mais cela désactive la réactivité — les changements ne déclencheront plus la recomposition. Pour une lecture unique sans abonnement, utilisez currentValue() à l'intérieur d'un snapshot sans lecture.
mutableIntStateOf est plus rapide car il ne nécessite pas de boxing de int en Integer. Avec des milliers de mises à jour par seconde (animation, défilement), la différence peut atteindre 30-50% du temps d'allocation. Pour les mises à jour rares (clics, saisie de texte), la différence est négligeable.
C'est possible mais déconseillé. Au lieu de MutableState, passez State (lecture seule) + un lambda onValueChange. Cela implémente le motif State Hoisting et rend le composant réutilisable. Les composants qui acceptent MutableState violent le flux de données unidirectionnel.
Implémentez l'interface MutableState et fournissez override var value avec un getter et un setter. Dans le setter, vous pouvez ajouter une validation ou une journalisation. Pour la rétrocompatibilité avec Compose Runtime, enveloppez votre implémentation personnalisée dans snapshotFlow ou utilisez snapshotIncrement.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi