Explicá el state hoisting y el flujo de datos unidireccional en Compose
Compose no tiene data binding bidireccional: el estado baja como parámetros, los eventos suben como lambdas, y elevar el estado solo lo mueve a donde ambos lados se encuentran.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Explicá el state hoisting y el flujo de datos unidireccional en Compose
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#La regla central: el estado baja, los eventos suben
Compose no tiene data binding bidireccional ni objetos de modelo observables a los que una vista se suscribe en silencio y muta. En cambio, se apoya en una única disciplina llamada flujo de datos unidireccional (UDF): el estado se pasa hacia abajo a través del árbol de composables como parámetros comunes, y cualquier cosa que haga el usuario se reporta hacia arriba como llamadas a parámetros lambda — eventos, no mutaciones. Un composable que sigue esta regla nunca mete la mano en sus propios parámetros para cambiarlos; solo lee lo que le dieron y reporta lo que pasó.
El state hoisting (elevación de estado) es la técnica mecánica que hace que un composable siga UDF: en lugar de que un composable sea dueño de un remember { mutableStateOf(...) } privado, movés ("elevás") ese estado hacia un llamador, y el composable recibe dos cosas en su lugar — el valor actual, y un callback para pedir un cambio. La forma canónica es value: T emparejado con onValueChange: (T) -> Unit. El composable se vuelve sin estado (stateless): no tiene memoria propia, así que las mismas entradas exactas siempre producen la misma salida exacta, que es precisamente lo que lo hace trivial de previsualizar, testear con screenshots y reutilizar en un contexto para el que no lo diseñaste originalmente.
#Con estado vs. sin estado, lado a lado
// Sin estado — este composable no es dueño de ningún estado propio. No puede// decidir nada; solo puede mostrar `name` y pedir uno nuevo.@Composablefun NameInput(name: String,onNameChange: (String) -> Unit,modifier: Modifier = Modifier,) {OutlinedTextField(value = name,onValueChange = onNameChange,modifier = modifier,label = { Text("Nombre") },singleLine = true,)}// Wrapper con estado — este es el composable que decide DÓNDE vive realmente// el estado, y es lo único que cambia si esa respuesta cambia.@Composablefun NameInputScreen() {var name by rememberSaveable { mutableStateOf("") }NameInput(name = name, onNameChange = { name = it })}
Nada de NameInput cambió cuando decidimos que el estado debía vivir en rememberSaveable en lugar de, digamos, un StateFlow de un ViewModel — porque NameInput nunca tocó el estado directamente en primer lugar. Ese desacople es todo el beneficio de la elevación de estado: el composable que dibuja la UI y el composable (o clase) que es dueño del estado pueden evolucionar por separado, y podés escribir un preview de Compose o un test unitario de NameInput con solo pasarle un String fijo y una lambda que no hace nada, sin ninguna configuración adicional.
#¿Hasta dónde hay que elevar el estado?
Un error común es pensar que "elevar estado" siempre significa "ponerlo en un ViewModel". La regla real es más acotada y más útil: elevá el estado hasta el ancestro común más bajo entre todos los composables que necesitan leerlo o escribirlo, y no más allá. Si dos composables hermanos necesitan compartir el índice de una pestaña seleccionada, elevarlo hasta el composable padre que comparten alcanza — no hay ninguna razón para hacerlo pasar por un ViewModel solo porque una regla general lo sugiera. El estado solo necesita llegar a un ViewModel (u otra capa que sobreviva cambios de configuración y viva más que la composición) cuando tiene que sobrevivir la muerte del proceso, compartirse entre pantallas, o depender de lógica de negocio y fuentes de datos que la capa de UI no debería conocer.
#El ciclo completo, de punta a punta
En una pantalla real respaldada por un ViewModel, el flujo de datos unidireccional se convierte en un ciclo en vez de una calle de un solo sentido: la UI llama a una función del ViewModel en respuesta a una acción del usuario, el ViewModel corre lógica de negocio (posiblemente a través de un use case o un repositorio), produce un nuevo UiState inmutable, y ese nuevo estado baja de nuevo a través de los parámetros de los composables, provocando que recompongan exactamente las partes del árbol que lo leen.
Fijate en la dirección de cada flecha: el estado solo viaja hacia abajo hacia la UI, y solo los eventos viajan hacia arriba saliendo de ella. No hay ninguna flecha donde la UI meta la mano de costado en un objeto mutable compartido y lo cambie directamente — esa ausencia es toda la arquitectura.
#Una tabla que vale la pena memorizar para entrevistas
| Capa | ¿Es dueña del estado? | Recibe | Emite |
|---|---|---|---|
Composable sin estado (NameInput) | No | value, onValueChange | Nada — es una hoja |
Composable wrapper con estado (NameInputScreen) | Sí, vía remember/rememberSaveable | Nada desde afuera | Dibuja el composable sin estado con el estado actual + un setter |
| Pantalla respaldada por ViewModel | Sí, vía StateFlow/SavedStateHandle | Eventos de UI (llamadas a funciones) | UiState (recolectado con collectAsStateWithLifecycle) |