Curso de Patrones MVVM en Android

Deserialización de JSON a UI en Android

Curso de Patrones MVVM en Android

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-okhttp con su versión en el catálogo de dependencias.
  • Declarar implementation(libs.coil.compose) y libs.coil.network.okhttp en el build.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 Column raíz con clip, shadow, background y clickable.
  • Una Row interna con horizontalArrangement = SpaceBetween y verticalAlignment = CenterVertically.
  • Dentro, otra Row con Modifier.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 con state.copy(query = event.query).
  • OnSearch: dispara executeSearch().
  • 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.