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
Pantalla de búsqueda con MVVM en Compose
Resumen
Construir una pantalla de búsqueda en Jetpack Compose se vuelve mucho más limpio cuando aplicas componentes atómicos y separas el consumo de la API en su propio ViewModel. Aquí verás cómo armar un Search Screen que conecta con OpenFood API respetando el principio de responsabilidad única dentro del patrón MVVM.
La idea es simple: cada pieza hace una sola cosa. Y eso, aunque suene básico, es lo que mantiene tu app mantenible, testeable y desacoplada.
¿Por qué aplicar el principio de responsabilidad única en MVVM?
Cuando trabajas con MVVM, cada componente debería encargarse de una sola tarea. El principio de responsabilidad única establece que una clase, módulo o componente está encargado de realizar una única tarea dentro del sistema [0:30].
En la práctica, esto significa que tu Search Screen dibuja la UI, tu SearchViewModel maneja la lógica de búsqueda, y tus use cases ejecutan la llamada a la API. Cada feature gana independencia.
¿Qué ventaja real da aplicar SRP en MVVM? Tu módulo queda desacoplado, puedes escribir pruebas unitarias más limpias y cada feature evoluciona sin romper a las demás.
¿Cómo crear un componente atómico SearchTextField en Compose?
Dentro de presentation creas un paquete search y dentro otro llamado components. El primer componente atómico es SearchTextField, que solo dibuja el campo de texto donde el usuario va a buscar [1:10].
Este composable recibe varios parámetros que lo hacen reutilizable:
text: Stringcon el valor actual del campo.onValueChange: (String) -> Unitpara trackear cambios.onSearch: () -> Unitque ejecuta la búsqueda.modifier: Modifierpara personalizar desde fuera.hint: Stringcon el texto guía obtenido porstringResource.shouldShowHint: Booleanque oculta el hint cuando el campo tiene foco.onFocusChange: (FocusState) -> Unitpara reaccionar al foco.
Dentro de un Box colocas un BasicTextField. Aquí entra un detalle importante: con keyboardOptions defines ImeAction.Search para que el teclado muestre la lupa, y con keyboardActions enlazas esa lupa con el onSearch que recibiste por parámetro [3:40].
¿Qué modificadores le dan estilo al campo de búsqueda?
El BasicTextField se decora con varios modificadores encadenados:
clip(RoundedCornerShape(5.dp))para esquinas redondeadas.padding(2.dp)y unshadowconelevation = 2para una sombra sutil.background(MaterialTheme.colorScheme.surface)para respetar el tema.fillMaxWidth()para ocupar todo el ancho disponible.padding(spacing.spaceMedium)con el spacing local.onFocusChangedenlazado al callback recibido.testTagpara que Android reconozca este campo como search.
Luego, con una condición sobre shouldShowHint, muestras un Text con estilo MaterialTheme.typography.bodyLarge, FontWeight.Light y color LightGray, alineado al centro con padding inicial.
Para cerrar, agregas un IconButton alineado a Alignment.CenterEnd con Icons.Default.Search como imageVector y un contentDescription desde stringResource.
¿Cómo se arma la pantalla SearchScreen usando ese componente?
Una vez tienes el componente atómico, creas SearchScreen como un composable que recibe snackbarHostState, mealName: String, dayOfTheMonth: Int, month: Int, year: Int, onNavigateUp: () -> Unit y un viewModel [7:20].
Dentro defines dos variables clave:
spacingconLocalSpacing.currentpara el espaciado consistente.keyboardControllerconLocalSoftwareKeyboardController.currentpara esconder el teclado tras la búsqueda.
La raíz es una Column con fillMaxSize() y padding(spacing.spaceMedium). Adentro pones un Spacer con altura spaceLarge, un Text que muestra el mealName con stringResource(R.string.add_meal, mealName) y estilo titleMedium, otro Spacer, y luego el SearchTextField que ya construiste.
En el onSearch del SearchTextField llamas keyboardController?.hide() para que el teclado desaparezca cuando el usuario dispare la búsqueda.
¿Por qué esconder el teclado tras buscar? Porque mejora la experiencia: el usuario ya disparó la acción, así que liberar la pantalla del teclado le permite ver los resultados sin fricción.
¿Cómo conectar el SearchViewModel con la OpenFood API?
Dentro del paquete search creas SearchViewModel, anotado con @HiltViewModel y heredando de ViewModel. Por constructor inyectas los TrackerUseCases que ya tenías [11:50].
Declaras un Channel<UiEvent> privado y lo expones como Flow con receiveAsFlow(). Después defines la función executeSearch() que dentro de viewModelScope.launch invoca el caso de uso correspondiente, en el ejemplo buscando la palabra pizza para validar la integración.
En SearchScreen recibes el viewModel con hiltViewModel(). Cada vez que el usuario presione la lupa del SearchTextField, ese evento llega al ViewModel y dispara la llamada a OpenFood API. Así, la pantalla no sabe nada del API: solo dispara intenciones.
¿Qué falta para navegar hasta el SearchScreen?
En la capa de core agregas un objeto serializable SearchScreenRoute para la navegación. Luego, en TrackerOverviewScreen añades una lambda onNavigateToSearch: () -> Unit que se ejecuta cuando el usuario toca breakfast, lunch o dinner.
En el NavHost enlazas esa lambda con navHostController apuntando a SearchScreenRoute, y agregas el composable correspondiente que renderiza SearchScreen pasándole snackbarHostState, mealName, dayOfTheMonth = 1, month = 1, year = 2022 y onNavigateUp.
Al correr el emulador, tocas breakfast, llegas al Search Screen, presionas la lupa y ves un status 200 con el JSON response del OpenFood API. Confirmación de que la integración quedó en el lugar correcto.
Mantener cada componente atómico y cada ViewModel enfocado en una sola feature es lo que vuelve a una app de Compose realmente escalable. ¿Tú cómo organizas la separación entre UI y lógica en tus proyectos MVVM? Cuéntame en los comentarios.