Contenido del curso
Implementación en MVVM
- 4

Navegación y pantallas de onboarding en Compose
26:17 min - 5

Grafo de navegación en Jetpack Compose
12:05 min - 6

Creación de ViewModels para Interacción en Onboarding de Apps
15:56 min - 7

View Models en Android: Gestión de la Lógica de Negocio y UI
17:20 min - 8

Hilt y SharedPreferences en ViewModels Android
29:30 min
Pantallas de Seguimiento
Networking y Datos
Persistencia Local
Funcionalidades Avanzadas
Lanzamiento de la APP
Casos de uso con Flows en MVVM Android
Resumen
Los casos de uso en el patrón MVVM son piezas clave cuando aplicas arquitectura limpia en Android, porque encapsulan la lógica de negocio y mantienen al ViewModel enfocado en preparar datos para la UI. Aquí verás cómo construir un caso de uso para traer comidas por fecha usando Flows de Kotlin, Room y Hilt, dentro de una app de tracking nutricional.
¿Por qué son importantes los casos de uso en MVVM?
Los casos de uso interactúan con las reglas de la aplicación y devuelven la información que el ViewModel necesita, ya sea desde la API o desde la base de datos local. Al separarlos en clases independientes, tu app gana testeabilidad, mantenibilidad y escalabilidad.
Un ejemplo claro aparece en el Calculate Meal Nutrients Use Case [02:00], que se ejecuta al terminar el onboarding. Ese caso de uso aplica la fórmula BMR (Basal Metabolic Rate), la tasa metabólica basal, que representa la cantidad mínima de energía que un cuerpo necesita para realizar funciones vitales. Con el peso, la altura, el objetivo (ganar, perder o mantener), la actividad física y los porcentajes de carbohidratos, proteínas y grasas, calcula el objetivo diario de calorías que verás en la tracker overview screen bajo your goal.
¿Todos los proyectos móviles necesitan casos de uso? No. En apps pequeñas suelen ser opcionales. En apps robustas con mucha lógica de negocio en el front, son altamente recomendables porque separan responsabilidades y facilitan pruebas.
¿Cómo traigo comidas por fecha desde Room con Flows?
La capa de datos arranca en el DAO. Necesitamos un método que devuelva las comidas guardadas filtradas por día, mes y año, y que se actualice en tiempo real en la pantalla.
El método getFoodsForDate recibe day, month y year como enteros, y retorna un Flow<List<TrackedFoodEntity>> [05:30]. Encapsular en Flow es clave: cuando el usuario guarda o cambia una comida, la UI refleja el cambio de inmediato.
kotlin @Query("SELECT * FROM TrackedFoodEntity WHERE dayOfMonth = :day AND month = :month AND year = :year") fun getFoodsForDate(day: Int, month: Int, year: Int): Flow<List<TrackedFoodEntity>>
Los Flows son streams de datos que fluyen por un túnel del que tomas lo que necesitas en el momento. Existen dos tipos:
- Hot flows: se recolectan todo el tiempo.
- Cold flows: requieren recolección explícita.
- Ambos forman parte del API reactivo de Kotlin, similar a los streams de Java.
¿Cómo conecto el repositorio con el dominio?
En la capa de dominio agregas getFoodsForDate(localDate: LocalDate): Flow<List<TrackedFood>> a la interfaz del repositorio. No marques la función con suspend: los Flows ya manejan corrutinas en su ciclo de vida.
En la implementación dentro de la capa de data, llamas al DAO y mapeas las entidades:
kotlin override fun getFoodsForDate(localDate: LocalDate): Flow<List<TrackedFood>> { return dao.getFoodsForDate( day = localDate.dayOfMonth, month = localDate.monthValue, year = localDate.year ).map { entities -> entities.map { it.toTrackedFood() } } }
Nota que el mapeo va de TrackedFoodEntity (base de datos) a TrackedFood (dominio), no a TrackableFood. Confundir estos tipos es un error común.
¿Cómo creo el caso de uso y lo inyecto con Hilt?
El caso de uso GetFoodsForDateUseCase recibe el repositorio por constructor y expone un operator fun invoke para llamarlo como si fuera una función [12:45].
kotlin class GetFoodsForDateUseCase( private val trackerRepository: TrackerRepository ) { operator fun invoke(date: LocalDate): Flow<List<TrackedFood>> { return trackerRepository.getFoodsForDate(date) } }
Luego lo agregas a la data class que empaqueta los casos de uso junto con CalculateMealNutrientsUseCase, y los inyectas con Hilt para que cualquier ViewModel los reciba sin fricción.
¿Qué es un operator fun invoke en un caso de uso? Es una convención de Kotlin que permite invocar una instancia como si fuera una función. Así puedes escribir
useCase(date)en lugar deuseCase.execute(date).
¿Cómo manejo eventos, estado y jobs en el ViewModel?
En la capa de presentación defines dos piezas: el evento y el estado. TrackerOverviewEvent es una sealed class con OnToggleMealClick, que expande visualmente breakfast, lunch o dinner y muestra el botón para navegar a la búsqueda.
TrackerOverviewState es una data class que guarda los totales y los objetivos del día:
totalCarbs,totalProtein,totalFat,totalCalories.carbsGoal,proteinGoal,fatGoal,caloriesGoal.date: LocalDate = LocalDate.now().trackedFoods: List<TrackedFood> = emptyList().meals: List<Meal> = defaultMeals.
¿Por qué necesito un Job para los Flows?
Los Flows directos requieren un Job que los hospede dentro de una corrutina. El Job es la unidad donde corre el Flow y permite cancelarlo, pausarlo o reiniciarlo. Los Channels, en cambio, ya traen ese ciclo de vida embebido [18:20].
En la función refreshFoods del ViewModel, primero cancelas el Job anterior por seguridad y luego asignas el nuevo:
kotlin private var getFoodsForDateJob: Job? = null
private fun refreshFoods() { getFoodsForDateJob?.cancel() getFoodsForDateJob = trackerUseCases.getFoodsForDate(state.date) .onEach { foods -> /* calcular nutrientes y actualizar state */ } .launchIn(viewModelScope) }
Llama refreshFoods() desde el bloque init del ViewModel para que la pantalla cargue datos al instante.
¿Cómo construyo el botón Add en Jetpack Compose?
El componente AddButton es un composable con Row envuelto en clip(RoundedCornerShape(100f)), borde de 1 dp, color primario del tema, e ícono Icons.Default.Add seguido de un Text con tipografía del MaterialTheme. Este botón aparece cuando el usuario toca breakfast, lunch o dinner, y navega a la pantalla de búsqueda.
Un detalle práctico: dentro de TrackerOverviewScreen, filtra state.trackedFoods por mealType para mostrar solo las comidas correspondientes a cada sección. Y cuida no envolver el botón dentro del forEach de comidas, porque mientras la lista esté vacía, el botón no aparecerá.
Cuéntame en los comentarios: ¿en qué punto de tu app crees que un caso de uso te ahorraría más dolores de cabeza?