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
Deserialización de JSON a UI en Android
Resumen
Mostrar resultados de búsqueda con datos reales de un API requiere algo más que pintar texto en pantalla. Necesitas deserializar correctamente la respuesta JSON para que tu app entienda qué datos llegan, cuáles pueden faltar y cómo renderizarlos sin romperse. Esta guía te lleva paso a paso por la conexión entre OpenFoodFacts, Kotlin y Jetpack Compose para construir una pantalla de búsqueda funcional.
¿Qué es la deserialización y por qué importa en tu app Android?
La deserialización es el preprocesamiento que transforma la respuesta de un servicio REST al lenguaje que tu aplicación entiende. En este caso, convertimos el JSON que llega desde la API de OpenFood en objetos Kotlin que Compose puede renderizar [02:00].
¿Qué es la deserialización? Es el proceso de convertir datos de un formato externo (como JSON) en objetos del lenguaje de tu app. Si no la haces bien, valores faltantes pueden romper la pantalla.
La clave está en aceptar que no todos los campos del JSON llegan siempre. Algunas llaves vienen, otras no. Si tu modelo no contempla esa nulabilidad, la app revienta al primer campo ausente.
¿Cómo cargar imágenes desde una URL con Coil en Compose?
Para renderizar las imágenes que devuelve el API necesitas una librería de carga. Aquí entra Coil, una solución ligera y nativa para Compose [02:45].
Los pasos para integrarla:
- Agregar el módulo
io.coil-kt.coil3:coil-network-okhttpcon su versión en el catálogo de dependencias. - Declarar
implementation(libs.coil.compose)ylibs.coil.network.okhttpen elbuild.gradle.kt. - Hacer sync para que Gradle descargue las librerías.
Con Coil instalado, puedes usar rememberAsyncImagePainter dentro de un composable Image para cargar URLs directamente.
¿Cómo manejar campos nulos en los modelos de data y dominio?
Acá viene lo interesante. En el modelo Nutriments de la capa de data y en TrackableFood del dominio, debes marcar como nulables los campos que el API podría no enviar [04:10]. Hablamos de calorías, carbohidratos, proteínas y grasas.
El mapper que conecta ambas capas también necesita ajuste. Aquí usas el operador Elvis (?:) para asignar un valor por defecto cuando llegue null.
kotlin val calculatedCalories = it.nutriments.carbohydrates100g?.times(4f) ?: 0f + it.nutriments.proteins100g?.times(4f) ?: 0f + it.nutriments.fat100g?.times(9f) ?: 0f
¿Para qué sirve el operador Elvis en Kotlin? Permite asignar un valor por defecto cuando una variable es null. Si el API no envía carbohidratos, el resultado será 0 en lugar de un crash.
La fórmula usa multiplicadores específicos: carbohidratos y proteínas se multiplican por 4, las grasas por 9, siguiendo el cálculo nutricional estándar de calorías por gramo.
¿Cómo construir el componente TrackableFoodItem con Compose?
El componente que muestra cada resultado es un composable con varias propiedades clave: el estado de UI, una lambda onClick, otra onAmountChange para la cantidad y una onTrack para registrar la comida [08:30].
Estructura visual del item
La jerarquía sigue este orden:
- Una
Columnraíz conclip,shadow,backgroundyclickable. - Una
Rowinterna conhorizontalArrangement = SpaceBetweenyverticalAlignment = CenterVertically. - Dentro, otra
RowconModifier.weight(1f)que contiene la imagen y los textos.
La imagen usa rememberAsyncImagePainter con tres parámetros importantes: el model apunta a food.imageUrl, mientras que placeholder y error muestran un ícono de respaldo si la URL falla o no llega.
Textos con manejo de overflow
El nombre de la comida usa MaterialTheme.typography.bodyLarge, maxLines = 1 y overflow = TextOverflow.Ellipsis. Así, cuando el texto excede el espacio, aparecen los tres puntos al final en lugar de cortarse abruptamente.
Para las calorías, validamos nulabilidad antes de pintar. Si food.calories es null, mostramos 0 kcal por cada 100g; si tiene valor, lo renderizamos directamente.
¿Cómo mostrar carbohidratos, proteínas y grasas en la UI?
Reutilizamos un componente llamado NutrientInfo que ya teníamos creado. Cada cajita recibe el nombre del nutriente, la cantidad, la unidad en gramos y los tamaños de texto personalizados [16:20].
Las tres cajitas se separan con Spacer(Modifier.width(Spacing.spaceSmall)) y se configuran así:
- Carbohidratos con
food.carbs ?: 0. - Proteínas con
food.protein ?: 0. - Grasas con
food.fat ?: 0.
El tamaño del texto se sobrescribe con amountTextSize = 16.sp y unitTextSize = 12.sp para que encajen visualmente con el resto del item.
¿Cómo conectar el ViewModel con la pantalla de búsqueda?
El SearchViewModel expone un state mutable de tipo SearchState con private set, y una función onEvent(event: SearchEvent) que maneja tres casos [22:40]:
OnQueryChange: actualiza la query del estado constate.copy(query = event.query).OnSearch: disparaexecuteSearch().OnSearchFocusChange: alterna la visibilidad del hint según el foco del campo.
¿Qué es un SearchEvent? Es una sealed class que representa cada acción posible del usuario en la búsqueda. Centralizar eventos así mantiene el ViewModel ordenado y predecible.
Dentro de executeSearch, primero marcas isSearching = true y vacías la lista. Luego llamas a trackerUseCases.searchFood(state.query), manejas el onSuccess mapeando los resultados a TrackableFoodUiState, y en el onFailure envías un UiEvent.ShowSnackbar con el recurso de string something_went_wrong.
¿Cómo renderizar la lista de resultados en SearchScreen?
En la pantalla, recolectas el state del ViewModel y configuras un LaunchedEffect que escucha los uiEvent. Cuando llega un ShowSnackbar, oculta el teclado con el keyboardController y muestra el mensaje [27:50].
El SearchTextField recibe el query del estado y dispara los eventos correspondientes en onValueChange, onSearch y onFocusChange. Debajo, una LazyColumn con Modifier.fillMaxSize() itera sobre state.trackableFood y pinta cada TrackableFoodItem con sus lambdas.
Al ejecutar el emulador y buscar "pizza", aparecen resultados reales con imágenes, carbohidratos y proteínas. Pero hay un detalle: todas las tarjetas muestran "hamburguesa con queso" como nombre, porque dejamos un texto quemado durante las pruebas.
¿Cómo lo corregirías tú para mostrar el nombre real de cada comida? Déjamelo en los comentarios.