mutableStateOf es una función en Jetpack Compose que crea un contenedor de estado mutable observable. Cuando el valor dentro de este contenedor cambia, Compose activa automáticamente la recomposición de todos los componentes que leen este estado. Sin mutableStateOf, la UI no podría actualizarse de forma reactiva cuando los datos cambian. Según Google Android Developers, 2026, mutableStateOf es el bloque de construcción principal para el estado local en Compose.
Puntos clave
mutableStateOf es una función del paquete compose.runtime que crea un objeto MutableState
MutableState es una interfaz con una única propiedad value: un getter para lectura y un setter para escritura. Cuando se llama al setter, Compose Runtime registra el cambio en un snapshot y marca todas las funciones Composable que leen esta variable State como necesitadas de recomposición. Este proceso ocurre de forma síncrona dentro de un único ciclo de snapshot, lo que elimina estados intermedios durante cambios en cascada.
Parámetro policy — el segundo argumento de mutableStateOf, define el comportamiento de comparación. structuralEquality() verifica equals() — este es el comportamiento por defecto. referentialEquality() verifica === (igualdad referencial). neverEqual() considera cada asignación como un cambio. La elección de policy afecta si se activa la recomposición al asignar el mismo valor.
La forma más simple de declarar estado observable es usar mutableStateOf con remember. Sin remember, cada recomposición crearía un nuevo State y todos los cambios anteriores se perderían. remember garantiza que el mismo MutableState sobreviva a una serie de recomposiciones mientras la función Composable permanezca en la composición.
@Composable
fun Counter() {
// Sin delegación: lectura/escritura mediante .value
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
@Composable
fun CounterDelegated() {
// Con delegación: var + by = Property Delegation
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
La diferencia entre los dos enfoques es sintáctica. Property Delegation (by) usa una convención de Kotlin: el compilador genera llamadas a getValue() y setValue() para lectura y escritura. Esto es equivalente a acceder directamente a count.value pero parece trabajar con una variable normal. Ambos enfoques son funcionalmente idénticos: Compose rastrea la lectura en el getter y la escritura en el setter independientemente de la notación.
| Forma | Código | Lectura | Escritura |
|---|---|---|---|
| Sin delegación | val count = mutableStateOf(0) | count.value | count.value = n |
| Con delegación | var count by mutableStateOf(0) | count | count = n |
El mecanismo de propiedades delegadas de Kotlin no es una característica de Compose sino una capacidad incorporada del lenguaje. Cualquier clase puede implementar los operadores getValue(thisRef, property) y setValue(thisRef, property, value), después de lo cual su instancia se puede usar con la palabra clave by. MutableState funciona exactamente así: getValue devuelve el valor actual y setValue asigna uno nuevo.
Una distinción importante: val vs var. mutableStateOf se puede asignar tanto a val como a var. Con val (val count = mutableStateOf(0)), el objeto MutableState en sí es inmutable, pero su propiedad value se puede cambiar. Con var (var count by mutableStateOf(0)), la delegación crea la ilusión de trabajar con un primitivo, pero el setter en realidad llama a setValue en MutableState. Elegir entre val y var es elegir entre acceso explícito e implícito a .value.
State Delegation es azúcar sintáctico que simplifica el código pero no cambia la mecánica. El compilador de Kotlin traduce var x by mutableStateOf(0) en getter/setter que llaman a mutableStateOf.getValue() y mutableStateOf.setValue(). En el bytecode generado, no hay diferencia entre val y var con by — ambos funcionan a través del mismo contenedor MutableState.
// Delegado personalizado para Compose State
class ValidatedState<T>(initialValue: T) {
private val state = mutableStateOf(initialValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (value != state.value) {
state.value = value
}
}
}
@Composable
fun Test() {
var text by remember { ValidatedState("") }
}
Snapshot es un mecanismo de Compose Runtime que garantiza la consistencia de lectura de State durante cambios paralelos. Cuando una función Composable lee mutableStateOf, el snapshot registra el valor actual. Si durante la composición otro cambio escribe en el mismo State, el snapshot ve la escritura pero no permite leer datos inconsistentes — la lectura siempre devuelve el valor válido al inicio del snapshot.
Cuando se llama al setter mutableStateOf.value = newValue, Compose Runtime no activa inmediatamente la recomposición. En su lugar, el cambio se registra en el snapshot actual. Cuando el snapshot se aplica (en el límite del frame), Compose recorre la lista de States cambiados y marca los componentes lectores como Invalid. Solo en el siguiente frame comienza la recomposición. Esto garantiza que la UI no se redibuje docenas de veces durante cambios en cascada.
Snapshots globales y locales: por defecto, mutableStateOf funciona en el snapshot global, que se aplica automáticamente. Puedes crear un snapshot local mediante Snapshot.takeSnapshot() para lectura aislada sin efectos secundarios. Esto se usa dentro de Modifier cuando necesitas leer State sin suscribirte a cambios. Este enfoque optimiza el rendimiento y previene recomposiciones inesperadas.
Consideremos un escenario real — un formulario de inicio de sesión con tres campos: correo electrónico, contraseña y estado de carga. Los tres campos usan mutableStateOf, pero con diferentes policy y diferentes niveles de anidación. email usa delegación, password usa acceso directo.
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun LoginForm(onLogin: (String, String) -> Unit) {
// Estado único para el formulario, policy = referentialEquality
var formState by remember {
mutableStateOf(LoginState(), SnapshotMutationPolicy.referentialEquality())
}
val isValid = remember(formState) {
formState.email.contains("@") && formState.password.length() >= 6
}
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = formState.email,
onValueChange = { formState = formState.copy(email = it) },
label = { Text("Correo electrónico") }
)
OutlinedTextField(
value = formState.password,
onValueChange = { formState = formState.copy(password = it) },
label = { Text("Contraseña") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = { onLogin(formState.email, formState.password) },
enabled = isValid
) {
Text("Iniciar sesión")
}
}
}
En este ejemplo, mutableStateOf se usa con una clase de datos personalizada LoginState y policy referentialEquality. Esto significa que la recomposición se activará solo cuando se asigne una nueva instancia de LoginState mediante copy(). isValid se calcula basándose en formState y se recalcula solo cuando cambia. Este enfoque proporciona un control claro sobre las recomposiciones: cada campo del formulario cambia solo mediante la creación de una nueva copia.
Preguntas frecuentes
mutableStateOf es un contenedor específico de Compose que funciona dentro de snapshots. StateFlow proviene de kotlinx.coroutines.flow y no está vinculado a Compose. mutableStateOf activa automáticamente la recomposición, mientras que StateFlow requiere collectAsState(). Para el estado de UI dentro de Composable, se prefiere mutableStateOf.
Sí, mutableStateOf se puede llamar fuera de funciones Composable, pero no será rastreado. Para la reactividad en la UI, se debe leer State dentro de Composable. Muchas ViewModel usan MutableStateField (un envoltorio sobre mutableStateOf) para pasar estado a la UI mediante StateFlow.
El sistema Snapshot garantiza consistencia: cada recomposición ve un estado consistente al inicio del snapshot. Los cambios de diferentes hilos se aplican atómicamente en el límite del frame, eliminando condiciones de carrera durante las lecturas dentro de una sola composición.
Asigna un nuevo valor: count.value = 0 (o count = 0 con delegación). Si necesitas recrear completamente el State, usa remember con una clave: remember(key) { mutableStateOf(initial) } — cuando la clave cambie, el State se creará de nuevo.
Composer usa snapshots que agrupan cambios: incluso con cientos de asignaciones en un solo frame, la recomposición se ejecuta solo una vez. Para actualizaciones muy frecuentes (animaciones), usa Animatable o animate*AsState — están optimizados para actualizaciones frame a frame.
Resumen
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.
Lea también