State Hoisting es un patrón en Jetpack Compose mediante el cual el estado se extrae de una función Composable hija y se traslada a la padre, mientras que la hija recibe datos a través de parámetros y notifica cambios mediante callbacks. Es una implementación del principio de flujo de datos unidireccional (UDF), donde el estado se eleva hacia arriba y los eventos descienden. Según Google Android Developers, 2026, State Hoisting hace que los componentes sean reutilizables, testeables y predecibles.
Puntos clave
State Hoisting es un patrón en el que una función Composable no posee el estado sino que lo recibe desde fuera. En lugar de usar var dentro de la función, se emplean dos parámetros: un valor para mostrar y un callback lambda para manejar los cambios. Técnicamente, esto significa que el componente hijo se vuelve stateless (sin estado propio), mientras que el padre es stateful (posee el estado).
Ejemplo: el componente TextField de Material3 no almacena el texto ingresado internamente. Acepta value: String y onValueChange: (String) -> Unit. El padre que llama a TextField declara var value by remember { mutableStateOf("") } y pasa value y onValueChange. Esto es State Hoisting clásico: TextField es un componente tonto (solo muestra y reporta la entrada), el padre es inteligente (posee el estado).
Stateless vs Stateful: Un componente stateless es más fácil de probar — no depende del estado interno, su comportamiento está totalmente determinado por los parámetros de entrada. Un componente stateful es conveniente para prototipado rápido pero más difícil de reutilizar: está fuertemente acoplado a una sola fuente de datos. State Hoisting te da la opción: cualquier componente puede hacerse stateless elevando el estado hacia arriba.
UDF (Unidirectional Data Flow) es un principio arquitectónico donde los datos se mueven en una dirección: desde la fuente de verdad (ViewModel o Composable padre) hacia la UI, y los eventos fluyen en dirección opuesta. State Hoisting es la implementación de UDF a nivel de componentes individuales. En lugar de que cada componente decida cuándo y cómo cambiar su propio estado, notifica al padre sobre un evento, y el padre decide cómo cambiar el estado.
Ventajas de UDF: predecibilidad — el estado cambia en un solo lugar, eliminando condiciones de carrera; trazabilidad — la pila de llamadas permite reconstruir la cadena de cambios; pruebas — la lógica stateful puede extraerse a una clase separada y probarse sin UI. En proyectos grandes, UDF combinado con State Hoisting es el estándar de facto.
Fuente única de verdad (Single Source of Truth) es otro principio que acompaña a UDF. Cada fragmento de estado tiene exactamente una fuente. Si dos componentes usan el mismo estado, la fuente debe compartirse (a nivel de ViewModel o padre común). State Hoisting asegura que la fuente esté arriba en la jerarquía y no se produzca duplicación de estado.
| Dirección | Qué se pasa | Cómo se implementa |
|---|---|---|
| Abajo (padre → hijo) | Valor a mostrar | Parámetro value: T |
| Arriba (hijo → padre) | Evento de cambio | Parámetro onValueChange: (T) -> Unit |
La regla principal: el estado debe elevarse al nivel mínimo posible suficiente para todos los componentes que lo necesiten. Si el estado solo se usa dentro de un componente — manténgalo local. Si dos componentes adyacentes necesitan el mismo estado — elévelo al padre común. Si el estado se necesita en toda la pantalla — elévelo a la ViewModel.
La regla de elevación mínima evita la complejidad innecesaria. No tiene sentido elevar el estado de un campo de texto a una ViewModel si solo se usa dentro de una pantalla y no se persiste al recrear la Activity. Use rememberSaveable a nivel del padre de la pantalla, no en ViewModel, para el estado de UI que debe sobrevivir a una rotación de pantalla pero que la lógica de negocio no necesita.
Cuándo elevar a ViewModel: si el estado debe sobrevivir a la recreación de Activity, si varios screens lo necesitan, si cambiar el estado dispara lógica de negocio (peticiones de red, base de datos). State Hoisting a nivel de ViewModel es un patrón estándar en la arquitectura MVVM, donde la capa de UI es stateless y la ViewModel es stateful.
// ❌ Mal: el componente posee su propio estado
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ Bien: State Hoisting — estado en el padre
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// Uso: el padre posee el estado
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
Considere una pantalla de inicio de sesión con dos campos (email, contraseña) y un botón. Los tres componentes reciben estado a través de State Hoisting: el email y la contraseña son gestionados por el padre, el botón recibe su estado enabled como valor.
// State Hoisting a nivel de pantalla
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// Campo Email — State Hoisting mediante lambda
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// Campo Contraseña — igualmente
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// Botón — recibe solo enabled (solo lectura)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// Componente Stateless: recibe email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("Email") },
singleLine = true
)
}
// Componente de botón Stateless
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("Iniciar sesión")
}
}
EmailField y PasswordField son completamente stateless. Pueden reutilizarse en cualquier pantalla conectándolos a cualquier fuente de datos. LoginButton recibe enabled como solo lectura — esta es otra forma de State Hoisting donde el estado no se eleva (el botón no puede habilitarse solo) sino que se pasa ya preparado. Este enfoque proporciona máxima flexibilidad con el mínimo acoplamiento entre componentes.
No todo estado necesita ser elevado. El estado local (State dentro de un Composable) está justificado cuando: los datos solo se necesitan dentro de un componente, no afectan a elementos hermanos y no deben sobrevivir a la recomposición de una sección específica. Por ejemplo, el estado de animación, el foco de un campo de entrada, la posición actual de desplazamiento — es razonable mantenerlos localmente.
Cuándo es necesario State Hoisting: el estado lo usan varios componentes hijos; un cambio en un hijo debe reflejarse en otro; la lógica de cambios de estado debe probarse separada de la UI; el estado debe sobrevivir a la recreación de Activity. En estos casos, el estado local crea duplicación e inconsistencia de datos.
Enfoque híbrido: mantenga el estado mínimo localmente, eleve el resto. La regla de Compose: “eleve el estado tan arriba como sea necesario y tan abajo como sea posible.” En la práctica, esto significa comenzar con remember local y solo elevar el nivel cuando se necesite acceso desde otro componente. No aplique State Hoisting de forma preventiva — complica el código sin necesidad.
Preguntas frecuentes
State Hoisting es un patrón a nivel de componentes de UI. ViewModel es una capa arquitectónica para la lógica de negocio. State Hoisting puede elevar el estado al nivel del Composable padre, al nivel de pantalla o a la ViewModel. ViewModel es el punto más alto de elevación para el estado que debe sobrevivir a la recreación de Activity.
Un componente stateless se prueba simplemente pasando valores. Llame al Composable con los parámetros requeridos y verifique la visualización mediante ComposeTestRule. Los cambios de estado se prueban a nivel del padre o de la ViewModel — separados de la UI. Esto simplifica significativamente las pruebas: no es necesario simular la recomposición dentro del componente.
Sí, es una práctica común. Si un componente solo necesita mostrar datos sin posibilidad de modificarlos — pase State<T> (solo lectura). El componente se suscribirá a los cambios pero no podrá iniciarlos. Esto fortalece la encapsulación y protege los datos de mutaciones no deseadas.
Para pasar en profundidad use CompositionLocal o pase a través de los parámetros del Composable padre. Si el estado se necesita en toda la pantalla — extráigalo a una ViewModel y use collectAsState(). Pasar a través de 5+ niveles es señal de arquitectura incorrecta; reconsidere la jerarquía de componentes.
State Hoisting puede aumentar ligeramente el número de recomposiciones, ya que un cambio en el padre puede recomponer a todos los hijos. Use derivedStateOf para filtrar cambios y keys en LazyColumn para actualizaciones selectivas. En la mayoría de escenarios, la sobrecarga de State Hoisting es insignificante comparada con el beneficio de mantenibilidad.
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