Curso de Patrones MVVM en Android

Interacción de Eventos y Estados en UI usando MVVM

Curso de Patrones MVVM en Android

Interacción de Eventos y Estados en UI usando MVVM

Resumen

Aprender a separar eventos y estados en el patrón MVVM te permite construir interfaces reactivas y mantenibles en Jetpack Compose. Si trabajas en una app de tracking de comida con Kotlin, dominar esta separación facilita que tu UI responda con claridad a cada acción del usuario, desde mostrar un loading hasta expandir un ítem al tocarlo.

Por qué separar eventos y estados en el ViewModel

Cuando trabajas con MVVM, el view model gestiona dos cosas distintas: lo que ocurre y lo que se ve. Mezclar ambos vuelve la lógica confusa y difícil de escalar.

¿Cuál es la diferencia entre evento y estado en MVVM? Un evento es algo que sucede y tiene lugar en la UI, como un tap o una búsqueda. Un estado es una condición particular en un momento determinado del componente visual, como isSearching = true.

Esta diferenciación específica hace que la lógica de interacción sea más concreta y objetiva. Tu pantalla deja de adivinar qué hacer y simplemente reacciona al estado actual [0:35].

Cómo mostrar un loading mientras se busca comida

En el SearchScreen se agrega un Box con modifier = fillMaxSize() y contentAlignment = Alignment.Center. Dentro, una condición evalúa el estado: si isSearching es verdadero, se muestra un CircularProgressIndicator [1:15].

Si la lista trackableFood resulta vacía, aparece un Text con StringResource que dice No results, estilizado con MaterialTheme.typography.bodyMedium y textAlign = Center. Antes, la app buscaba pero no mostraba feedback. Ahora el usuario ve tres estados claros:

  • Pantalla inicial con No results.
  • Loading mientras se hace la búsqueda.
  • Lista de resultados cuando hay coincidencias.

Este cambio convierte una UI muda en una experiencia intuitiva.

Cómo crear un evento OnToggleTrackableFood en Compose

Para que cada ítem de la lista reaccione al tap, necesitas un evento nuevo. En el archivo SearchEvent se agrega una data class OnToggleTrackableFood que recibe una variable de tipo TrackableFood y hereda de SearchEvent [3:05].

Este evento es el puente entre el toque del usuario y el cambio visual del ítem expandido.

Cómo actualizar el estado desde el ViewModel

En el ViewModel, dentro de la función onEvent, se maneja el nuevo caso con is SearchEvent.OnToggleTrackableFood. La actualización se hace con state = state.copy(...), mapeando la lista trackableFood:

  • Si el ítem coincide con el event.food, se cambia isExpanded a su valor opuesto con it.copy(isExpanded = !it.isExpanded).
  • Si no coincide, se devuelve it sin cambios.

¿Qué hace state.copy() en un ViewModel de Compose? Crea una copia inmutable del estado actual con los valores que cambian. Compose detecta la diferencia y recompone solo lo necesario, evitando renders innecesarios.

Esta validación entre false y true es lo que hace que un mismo ítem se expanda o contraiga sin tocar a los demás.

Cómo construir el componente expandido con BasicTextField

Dentro del TrackableFoodItem, al tocarlo se dispara onEvent(OnToggleTrackableFood(food)) pasando la comida específica para asegurar que se expanda solo esa.

En la Row vacía que estaba reservada, se construye un BasicTextField con estas propiedades:

  • value y onValueChange conectados a amountChanged.
  • keyboardOptions con imeAction = Done cuando el amount es isNotBlank.
  • keyboardActions que ejecuta defaultKeyboardAction(ImeAction.Done) al pulsar la tecla principal.
  • singleLine = true para forzar una sola línea.

Cómo darle estilo y accesibilidad al campo de gramos

El modifier del BasicTextField incluye un border con RoundedCornerShape(5.dp), ancho de 0.5.dp y color MaterialTheme.colorScheme.onSurface. Se alinea con alignBy(LastBaseline) y se le añade padding con spacingMedium.

La propiedad semantics agrega un contentDescription = "Amount", pensado en accesibilidad para personas que usan lectores de pantalla. El sistema describirá que hay un campo de texto que recibe un amount [8:40].

Después del campo, un Spacer con width = spacingExtraSmall separa el texto que muestra gramos con MaterialTheme.typography.bodyLarge y alignBy(LastBaseline).

Cómo agregar el botón de check para trackear

Un IconButton cierra el componente con:

  • onClick = onTrack para disparar la acción de guardado.
  • enabled = trackableFoodUiState.amount.isNotBlank() para habilitarlo solo cuando hay valor.
  • Icon con ImageVector = Icons.Default.Check y contentDescription = R.string.track.

Usando el preview de Compose con isExpanded = true, ya puedes ver el componente desplegado con caja de texto, gramos y check listo para registrar la comida en la base de datos.

Por qué este patrón mejora tu app de tracking

La separación clara entre eventos y estados te da varias ventajas concretas:

  • Cada UI sabe exactamente qué muestra y qué dispara.
  • El view model concentra la lógica sin contaminar la vista.
  • Probar y depurar se vuelve directo porque cada estado es predecible.
  • Agregar nuevas interacciones, como un botón de retroceso, sigue el mismo patrón sin reescribir nada.

¿Te animas a aplicar el mismo principio para agregar la acción de volver desde la pantalla de búsqueda al tracker home screen? Cuéntame en los comentarios cómo lo resolviste.