MutableState — estado observable y mecanismo de actualización en Compose

Autor: IT Sectr Publicado: 2026-06-28 Tiempo de lectura: 7 min

MutableState es una interfaz en Jetpack Compose que representa un contenedor para un valor mutable observable. Es la base del sistema reactivo de Compose: cada vez que el valor de MutableState cambia a través del setter, Compose Runtime notifica a todos los componentes lectores y desencadena la recomposición. Según Google Android Developers, 2026, comprender MutableState es esencial para trabajar correctamente con el estado en una UI declarativa.

Puntos clave

  • MutableState — interfaz de compose.runtime con una única propiedad value (getter + setter)
  • State — interfaz padre de solo lectura, MutableState añade capacidad de escritura
  • Recomposición se activa al llamar al setter de value dentro de un ciclo de snapshot activo
  • SnapshotMutationPolicy determina cuándo un cambio se considera significativo para la recomposición
  • MutableIntState y similares — versiones primitivas optimizadas de MutableState

Qué es MutableState en Jetpack Compose

MutableState es una interfaz del paquete androidx.compose.runtime que declara una única propiedad: override var value: T. El getter devuelve el valor actual, el setter escribe uno nuevo y notifica a Compose Runtime del cambio. La interfaz hereda de State<T>, donde value es de solo lectura. Esta arquitectura de dos niveles permite separar el acceso: un componente que solo necesita leer el valor recibe State<T>, mientras que el componente propietario recibe MutableState<T>.

La implementación predeterminada de MutableState es la clase interna SnapshotMutableStateImpl, que utiliza un mecanismo de snapshots para rastrear cambios. Cuando se llama al setter de value, el snapshot actual registra la escritura y marca todos los ObservedScope registrados como inválidos. Estos ámbitos (normalmente funciones Composable) se recompondrán en el siguiente frame. Todo el proceso ocurre de forma sincrónica y sin bloqueos gracias a la arquitectura Lock-free snapshot.

State vs MutableState: State es una interfaz de solo lectura utilizada para las API públicas de componentes. Cuando declaras un parámetro de una función Composable como State<Int>, garantizas que el componente puede leer pero no cambiar el estado. MutableState se usa dentro del componente propietario. Esta separación es una de las prácticas básicas de Compose que previene cambios no autorizados.

Jerarquía de State, MutableState e interfaces derivadas

La jerarquía de interfaces de estado en Compose tiene varios niveles. En la cima se encuentra State<T> con un valor de solo lectura. Debajo está MutableState<T> con un valor de lectura-escritura. Más abajo vienen las versiones primitivas especializadas: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState y otras, que evitan el boxing de primitivos.

MutableDoubleState y MutableLongState son menos comunes pero también existen. Interfaces de colecciones: MutableListState — para rastrear cambios dentro de una lista, MutableStateMap — para mapas. Cada una de estas interfaces está optimizada para un escenario específico y extiende MutableState base con métodos adicionales de manipulación de colecciones.

SnapshotStateList y SnapshotStateMap son implementaciones de listas y mapas mutables compatibles con snapshots. Permiten rastrear no solo el reemplazo de valor, sino cambios internos: añadir un elemento a una lista, eliminar, modificar un elemento existente. Para estas estructuras, mutableStateListOf() y mutableStateMapOf() crean las colecciones observables correspondientes.

InterfazPropósitoMétodo de creación
State<T>Contenedor de solo lectura
MutableState<T>Contenedor de lectura-escrituramutableStateOf()
MutableIntStateInt primitivo sin boxingmutableIntStateOf()
MutableFloatStateFloat primitivo sin boxingmutableFloatStateOf()
SnapshotStateListLista observablemutableStateListOf()
SnapshotStateMapMapa observablemutableStateMapOf()

SnapshotMutationPolicy: cuándo activar la recomposición

SnapshotMutationPolicy es una interfaz que determina cuándo un cambio en MutableState se considera significativo. mutableStateOf acepta policy como segundo argumento. Implementaciones estándar: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (siempre considera el cambio). Se puede implementar una política personalizada para lógica propia.

structuralEquality() — comportamiento predeterminado. Compose compara el nuevo valor con el anterior mediante equals(). Si el resultado es true, la recomposición NO se activa. Esto es útil para primitivos y data classes, donde dos instancias con los mismos campos se consideran iguales. Inconveniente: si una data class contiene una List, equals() realiza una comparación profunda, que puede ser costosa para listas grandes.

referentialEquality() — compara referencias mediante ===. La recomposición se activa solo cuando se asigna un objeto diferente, incluso si el contenido es idéntico. Es óptimo para data classes inmutables donde cada nueva instancia garantiza un cambio. neverEqualPolicy() — siempre considera el cambio significativo sin realizar comparación. Útil cuando el setter se llama raramente y no es necesario gastar tiempo en equals.

kotlin
    // 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)
}

State primitivos: MutableIntState, MutableFloatState, MutableLongState

MutableIntState primitivo y similares son interfaces especializadas que almacenan primitivos sin boxing. Un MutableState<Int> normal almacena Int como Integer, lo que crea un objeto en el heap con cada escritura. MutableIntState almacena int (primitivo), eliminando por completo la sobrecarga de boxing. Esto es especialmente importante para actualizaciones de alta frecuencia — contadores, posiciones de scroll, valores de animación.

mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — funciones que crean MutableState primitivos. Las interfaces se llaman MutableIntState, MutableFloatState, MutableLongState. Extienden MutableState<Int>, MutableState<Float> y MutableState<Long> respectivamente, añadiendo la propiedad intValue para acceso rápido al primitivo. Su implementación interna utiliza AtomicInteger para lectura/escritura sin bloqueos.

Uso: contadores (Int), posiciones de scroll (Float offset), marcas de tiempo (Long). En la mayoría de escenarios cotidianos la diferencia de rendimiento es imperceptible, pero en LazyList con miles de elementos y animaciones de transición, los State primitivos proporcionan una mejora notable. Google recomienda usar State primitivos para escenarios típicos en lugar del mutableStateOf universal.

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

Ejemplos prácticos de trabajo con MutableState

Consideremos un componente TodoList, donde MutableState se usa en dos formas: como variables separadas para el estado de entrada y como SnapshotStateList para una lista dinámica de tareas. Ambas usan delegación para brevedad del código.

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("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 mutable que rastrea cambios en elementos individuales. Cuando se llama a items.add() e items[n] = newValue, Compose ve la mutación y recomponte solo aquellos elementos de LazyColumn que cambiaron. inputText es un MutableState<String> normal. La combinación de dos tipos de MutableState (individual y colección) es un patrón típico para pantallas con formularios y listas.

Preguntas frecuentes

¿Se puede usar MutableState sin remember?

MutableState sin remember se creará de nuevo en cada recomposición. Cada nueva llamada a mutableStateOf crea un nuevo objeto y el valor anterior se pierde. Usa siempre remember para conservar el State entre recomposiciones, a menos que el State se cree fuera de un Composable (por ejemplo, en un ViewModel).

¿Cómo convertir MutableState en una variable normal?

Lee .value una vez fuera de un snapshot mediante snapshot { }. Pero esto desactiva la reactividad — los cambios ya no desencadenarán la recomposición. Para una lectura única sin suscripción, usa currentValue() dentro de un snapshot sin lectura.

¿Qué es más rápido: mutableStateOf o mutableIntStateOf?

mutableIntStateOf es más rápido porque no requiere boxing de int a Integer. Con miles de actualizaciones por segundo (animación, scroll), la diferencia puede alcanzar el 30-50% en tiempo de asignación. Para actualizaciones poco frecuentes (clics, entrada de texto), la diferencia es insignificante.

¿Se puede usar MutableState como argumento de una función Composable?

Es posible pero no recomendado. En lugar de MutableState, pasa State (solo lectura) + una lambda onValueChange. Esto implementa el patrón State Hoisting y hace que el componente sea reutilizable. Los componentes que aceptan MutableState violan el flujo de datos unidireccional.

¿Cómo crear una implementación personalizada de MutableState?

Implementa la interfaz MutableState y proporciona override var value con un getter y setter. En el setter puedes añadir validación o registro. Para compatibilidad hacia atrás con Compose Runtime, envuelve tu implementación personalizada en snapshotFlow o usa snapshotIncrement.

Resumen

  • MutableState — interfaz básica para estado mutable observable en Compose
  • State — versión de solo lectura para pasar datos sin derecho a modificación
  • SnapshotMutationPolicy gestiona las condiciones para activar la recomposición al cambiar
  • State primitivos (MutableIntState y otros) eliminan la sobrecarga de boxing
  • SnapshotStateList y SnapshotStateMap rastrean cambios internos de colecciones
  • Recomposición se activa automáticamente ante cualquier llamada al setter de value
  • Recomendación: usa MutableState para poseer el estado y State para pasarlo hacia abajo

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también