Cómo obtener datos de Internet

1. Antes de comenzar

La mayoría de las apps para Android del mercado se conectan a Internet para realizar operaciones de red, como recuperar correos electrónicos, mensajes y más información de un servidor de backend. Gmail, YouTube y Google Fotos son ejemplos de apps que se conectan a Internet para mostrar los datos del usuario.

En este codelab, usarás bibliotecas de código abierto impulsadas por la comunidad para compilar una capa de datos y obtener datos de un servidor de backend. Esto simplifica en gran medida la recuperación de datos y también ayuda a la app a seguir las prácticas recomendadas de Android, como realizar operaciones en un subproceso en segundo plano. También mostrarás un mensaje de error si la conexión a Internet es lenta o no está disponible, lo que mantendrá al usuario informado sobre cualquier problema de conectividad de red.

Requisitos previos

  • Conocimientos básicos sobre cómo crear funciones de componibilidad
  • Conocimientos básicos sobre cómo usar los componentes de la arquitectura de Android ViewModel
  • Conocimientos básicos sobre cómo usar corrutinas para tareas de larga duración
  • Conocimientos básicos sobre cómo agregar dependencias en build.gradle.kts

Qué aprenderás

  • Qué es un servicio web REST
  • Cómo usar la biblioteca de Retrofit para conectarte a un servicio web de REST en Internet y obtener una respuesta
  • Cómo usar la biblioteca Serialization (kotlinx.serialization) para analizar la respuesta de JSON en un objeto de datos

Actividades

  • Modificarás una app de partida para realizar una solicitud a la API de servicio web y manejar la respuesta.
  • Implementarás una capa de datos para tu app usando la biblioteca Retrofit.
  • Analizarás la respuesta JSON del servicio web en la lista de objetos de datos de tu app con la biblioteca kotlinx.serialization y la adjuntarás al estado de IU.
  • Usarás la compatibilidad de Retrofit para las corrutinas a fin de simplificar el código.

Requisitos

  • Una computadora con Android Studio
  • El código inicial para la app de Mars Photos

2. Descripción general de la app

Trabajarás con la app llamada Mars Photos, que muestra imágenes de la superficie de Marte. Esta app se conecta a un servicio web para recuperar y mostrar fotos de Marte. Las imágenes son fotografías reales de Marte capturadas por los rovers marcianos de la NASA. La siguiente imagen es una captura de pantalla de la app final, que contiene una cuadrícula de imágenes.

68f4ff12cc1e2d81.png

La versión de la app que compiles en este codelab no tendrá mucho impacto visual. Este codelab se enfoca en la parte de la capa de datos de la app para conectarse a Internet y descargar los datos de propiedad sin procesar con un servicio web. Para asegurarte de que la app recupere y analice correctamente estos datos, puedes imprimir la cantidad de fotos recibidas del servidor de backend en un elemento componible Text.

a59e55909b6e9213.png

3. Explora la app de partida de Mars Photos

Descarga el código de partida

Para comenzar, descarga el código de partida:

Descargar ZIP

Como alternativa, puedes clonar el repositorio de GitHub para el código:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout starter

Puedes explorar el código en el repositorio de GitHub de Mars Photos.

Ejecuta el código de partida

  1. Abre el proyecto descargado en Android Studio. El nombre de la carpeta del proyecto es basic-android-kotlin-compose-training-mars-photos.
  2. En el panel de Android, expande app > java. Observa que la app tiene una carpeta de paquete llamada ui. Esta es la capa de la IU de la app.

d5102d14c37fc433.png

  1. Ejecuta la app. Cuando compiles y ejecutes la app, verás la siguiente pantalla con un texto de marcador de posición en el centro. Al final de este codelab, actualizarás este texto de marcador de posición con la cantidad de fotos recuperadas.

95328ffbc9d7104b.png

Explicación del código de partida

En esta tarea, te familiarizarás con la estructura del proyecto. En las siguientes listas, se proporcionan explicaciones de los archivos y las carpetas importantes del proyecto.

ui\MarsPhotosApp.kt:

  • Este archivo incluye el elemento de componibilidad MarsPhotosApp, que muestra el contenido en pantalla, como la barra de la app superior y el elemento de componibilidad HomeScreen. El texto del marcador de posición del paso anterior se muestra en este elemento de componibilidad.
  • En el siguiente codelab, este elemento de componibilidad mostrará los datos recibidos del servidor de backend con las fotos de Marte.

screens\MarsViewModel.kt:

  • Este archivo es el modelo de vista correspondiente para MarsPhotosApp.
  • Esta clase contiene una propiedad MutableState llamada marsUiState. Cuando se actualiza el valor de esta propiedad, se actualiza el texto del marcador de posición que se muestra en la pantalla.
  • El método getMarsPhotos() actualiza la respuesta del marcador de posición. Más adelante en el codelab, usarás este método para mostrar los datos recuperados del servidor. El objetivo de este codelab es que actualices el MutableState dentro de ViewModel con datos reales que obtengas de Internet.

screens\HomeScreen.kt:

  • Este archivo contiene los elementos de componibilidad HomeScreen y ResultScreen. ResultScreen tiene un diseño Box simple que muestra el valor de marsUiState en un elemento de componibilidad Text.

MainActivity.kt:

  • La única tarea de esta actividad es cargar el ViewModel y mostrar el elemento de componibilidad MarsPhotosApp.

4. Introducción a los servicios web

En este codelab, crearás una capa para el servicio de red que se comunique con el servidor backend y recupere los datos requeridos. Para implementar esta tarea, usarás una biblioteca de terceros llamada Retrofit. Obtendrás más información sobre este tema más adelante. El ViewModel se comunica con la capa de datos y el resto de la app es transparente para esta implementación.

3fe849669bd308da.png

MarsViewModel es responsable de realizar la llamada de red para obtener los datos de las fotos de Marte. En ViewModel, usa MutableState para actualizar la IU de la app cuando cambien los datos.

5. Servicios web y Retrofit

Los datos de las fotos de Marte se almacenan en un servidor web. Para obtener estos datos en tu app, debes establecer una conexión y comunicarte con el servidor en Internet.

301162f0dca12fcf.png

9d5543c4c7e85e9f.png

La mayoría de los servidores web ejecutan servicios web con una arquitectura web sin estado común conocida como REST o REpresentational State Transfer (transferencia de estado representacional). Los servicios web que ofrecen esta arquitectura se conocen como servicios RESTful.

Las solicitudes se realizan a los servicios web RESTful de manera estandarizada, a través de identificadores uniformes de recursos (URI). Un URI identifica un recurso en el servidor por nombre, sin implicar su ubicación ni cómo acceder a él. Por ejemplo, en la app de esta lección, recuperas las URLs de las imágenes con el siguiente URI del servidor (este servidor aloja las fotos y los inmuebles de Marte):

android-kotlin-fun-mars-server.appspot.com

Una URL (localizador de recursos uniforme) es un subconjunto de un URI que especifica la ubicación de un recurso y el mecanismo para recuperarlo.

Por ejemplo:

La siguiente URL obtiene una lista de las propiedades de bienes raíces disponibles en Marte.

https://android-kotlin-fun-mars-server.appspot.com/realestate

En la siguiente URL, se obtiene una lista de fotos de Marte:

https://android-kotlin-fun-mars-server.appspot.com/photos

Estas URLs hacen referencia a un recurso identificado, como /realestate (bienes raíces) o /photos (fotos), que se puede obtener mediante el Protocolo de transferencia de hipertexto (http:) de la red. En este codelab, usarás el extremo /photos. Un extremo es una URL que te permite acceder a un servicio web que se ejecuta en un servidor.

Solicitud de servicio web

Cada solicitud de servicio web contiene un URI y se transfiere al servidor mediante el mismo protocolo HTTP que usan los navegadores web, como Chrome. Las solicitudes HTTP contienen una operación para indicarle al servidor cómo proseguir.

Entre las operaciones comunes de HTTP se incluyen las siguientes:

  • GET para recuperar los datos del servidor
  • POST para crear datos nuevos en el servidor
  • PUT para actualizar los datos existentes en el servidor
  • DELETE para borrar datos del servidor

Tu app envía una solicitud GET HTTP al servidor para obtener información sobre las fotos de Marte y, luego, el servidor muestra una respuesta a tu app, incluidas las URLs de imágenes.

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

La respuesta de un servicio web tiene uno de los formatos de datos comunes, como XML (lenguaje de marcación extensible) o JSON (notación de objetos de JavaScript). El formato JSON representa datos estructurados en pares clave-valor. Una app se comunica con la API de REST mediante JSON, sobre lo que obtendrás más información en una tarea posterior.

En esta tarea, establecerás una conexión de red con el servidor, te comunicarás con el servidor y recibirás una respuesta JSON. Utilizarás un servidor backend que ya está escrito. En este codelab, usarás la biblioteca Retrofit, una biblioteca de terceros para comunicarte con el servidor backend.

Bibliotecas externas

Las bibliotecas externas o las bibliotecas de terceros son como las extensiones de las APIs de Android principales. Las bibliotecas que usarás en este curso son de código abierto, desarrolladas por la comunidad y mantenidas por las contribuciones colectivas de la enorme comunidad mundial de Android. Estos recursos ayudan a los desarrolladores de Android como tú a crear mejores apps.

Biblioteca Retrofit

La biblioteca Retrofit que utilizarás en este codelab para hablar con el servicio web RESTful de Marte es un buen ejemplo de una biblioteca bien mantenida. Para ello, ve a la página de GitHub y revisa los problemas abiertos y cerrados (algunas son solicitudes de funciones). Si los desarrolladores resuelven los problemas de forma periódica y responden a las solicitudes de funciones, es probable que la biblioteca tenga un buen mantenimiento y sea una buena candidata para usarla en tu app. También puedes consultar la documentación de Retrofit para obtener más información sobre la biblioteca.

La biblioteca de Retrofit se comunica con el backend de REST. Esta genera el código, pero tú debes proporcionar los URIs para el servicio web en función de los parámetros que le pasamos. Obtendrás más información sobre este tema en las secciones posteriores.

26043df178401c6a.png

Cómo agregar dependencias de Retrofit

Android Gradle te permite agregar bibliotecas externas a tu proyecto. Además de la dependencia de la biblioteca, también debes incluir el repositorio en el que se aloja.

  1. Abre el archivo de Gradle de nivel de módulo build.gradle.kts (Module :app).
  2. En la sección dependencies, agrega las siguientes líneas para las bibliotecas Retrofit:
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

Las dos bibliotecas funcionan juntas. La primera dependencia es para la biblioteca Retrofit2 y la segunda es para el conversor escalar de Retrofit. Retrofit2 es la versión actualizada de la biblioteca de Retrofit. Este conversor escalar permite que Retrofit muestre el resultado JSON como String. JSON es un formato para almacenar y transportar datos entre el cliente y el servidor. Aprenderás sobre JSON en una sección posterior.

  1. Haz clic en Sync Now para volver a compilar el proyecto con las dependencias nuevas.

6. Conexión a Internet

Usarás la biblioteca Retrofit para hablar con el servicio web de Marte y mostrar la respuesta JSON sin procesar como String. El marcador de posición Text mostrará la string de respuesta JSON que se muestra o un mensaje que indica un error de conexión.

Retrofit crea una API de red para la app basada en el contenido del servicio web. Recupera datos del servicio web y los enruta a través de una biblioteca de conversor independiente que sabe cómo decodificar los datos y mostrarlos en forma de objetos como String. Retrofit incluye compatibilidad integrada para formatos de datos populares, como XML y JSON. Retrofit crea el código para llamar y consumir este servicio, incluidos los detalles críticos, como la ejecución de solicitudes en subprocesos en segundo plano.

a78435c405c1e347.png

En esta tarea, agregarás una capa de datos a tu proyecto Mars Photos, que tu ViewModel usará para comunicarse con el servicio web. Implementarás la API del servicio de Retrofit con los siguientes pasos:

  • Crea una fuente de datos, clase MarsApiService.
  • Crea un objeto Retrofit con la URL base y la fábrica del conversor para convertir cadenas.
  • Crear una interfaz que explique cómo habla Retrofit con el servidor web.
  • Crea un servicio de Retrofit y expón la instancia del servicio de la API al resto de la app.

Implementa los pasos anteriores:

  1. Haz clic con el botón derecho en el paquete com.example.marsphotos del panel de tu proyecto de Android y selecciona New > Package.
  2. En la ventana emergente, agrega network al final del nombre del paquete sugerido.
  3. Crea un nuevo archivo de Kotlin debajo del nuevo paquete. Asígnale el nombre MarsApiService.
  4. Abre network/MarsApiService.kt.
  5. Agrega la siguiente constante para la URL base del servicio web.
private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. Agrega un compilador de Retrofit justo debajo de esa constante para compilar y crear un objeto Retrofit.
import retrofit2.Retrofit

private val retrofit = Retrofit.Builder()

Retrofit requiere el URI base para el servicio web y una fábrica de conversión para crear una API de servicios web. El conversor le indica a Retrofit qué hacer con los datos que obtiene del servicio web. En este caso, Retrofit tendría que recuperar una respuesta JSON del servicio web y mostrarla como String. Retrofit tiene un ScalarsConverter que admite cadenas y otros tipos primitivos.

  1. Llama a addConverterFactory() en el compilador con una instancia de ScalarsConverterFactory.
import retrofit2.converter.scalars.ScalarsConverterFactory

private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
  1. Agrega la URL base para el servicio web con el método baseUrl().
  2. Por último, llama a build() para crear el objeto Retrofit.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. Debajo de la llamada al compilador de Retrofit, define una interfaz llamada MarsApiService, que define cómo Retrofit se comunica con el servidor web mediante solicitudes HTTP.
interface MarsApiService {
}
  1. Agrega una función llamada getPhotos() a la interfaz MarsApiService para obtener la string de respuesta del servicio web.
interface MarsApiService {
    fun getPhotos()
}
  1. Usa la anotación @GET para indicar a Retrofit que es una solicitud GET y especifica el extremo para ese método de servicio web. En este caso, el extremo es photos. Como se mencionó en la tarea anterior, usarás el extremo /photos en este codelab.
import retrofit2.http.GET

interface MarsApiService {
    @GET("photos")
    fun getPhotos()
}

Cuando se invoca el método getPhotos(), Retrofit agrega el extremo photos a la URL base (que definiste en el compilador de Retrofit) que se usó para iniciar la solicitud.

  1. Agrega un tipo de datos que se muestra de la función a String.
interface MarsApiService {
    @GET("photos")
    fun getPhotos(): String
}

Declaraciones de objetos

En Kotlin, las declaraciones de objetos se usan para declarar objetos singleton. El patrón de singleton garantiza que una y solo una instancia de un objeto se cree y tiene un punto de acceso global a ese objeto. La inicialización del objeto es segura para los subprocesos y se realiza durante el primer acceso.

A continuación, se muestra un ejemplo de una declaración de objeto y su acceso. La declaración del objeto siempre tiene un nombre que sigue a la palabra clave object.

Ejemplo:

// Example for Object declaration, do not copy over

object SampleDataProvider {
    fun register(provider: SampleProvider) {
        // ...
    }
​
    // ...
}

// To refer to the object, use its name directly.
SampleDataProvider.register(...)

La función create() de un objeto Retrofit es costosa en términos de memoria, velocidad y rendimiento. La app necesita solo una instancia del servicio de la API de Retrofit, por lo que debes exponer el servicio al resto de la app mediante la declaración de objeto.

  1. Fuera de la declaración de la interfaz de MarsApiService, define un objeto público llamado MarsApi para inicializar el servicio de Retrofit. Este es el objeto singleton público al que puede acceder el resto de la app.
object MarsApi {}
  1. Dentro de la declaración del objeto MarsApi, agrega una propiedad de objeto de Retrofit inicializada de forma diferida y con el nombre retrofitService del tipo MarsApiService. Realizarás esta inicialización diferida para asegurarte de que se inicialice en su primer uso. Ignora el error, ya que lo corregirás en los próximos pasos.
object MarsApi {
    val retrofitService : MarsApiService by lazy {}
}
  1. Inicializa la variable retrofitService usando el método retrofit.create() con la interfaz MarsApiService.
object MarsApi {
    val retrofitService : MarsApiService by lazy {
       retrofit.create(MarsApiService::class.java)
    }
}

Se completó la configuración de Retrofit. Cada vez que tu app llame a MarsApi.retrofitService, el llamador accederá al mismo objeto singleton de Retrofit que implementa MarsApiService, que se crea en el primer acceso. En la próxima tarea, usarás el objeto Retrofit que implementaste.

Llama al servicio web en MarsViewModel

En este paso, implementarás el método getMarsPhotos() que llama al servicio de REST y, luego, controla la cadena JSON que se muestra.

ViewModelScope

Un viewModelScope es el alcance integrado de corrutinas definido para cada ViewModel en tu app. Si se borra ViewModel, se cancela automáticamente cualquier corrutina iniciada en este alcance.

Puedes usar viewModelScope para iniciar la corrutina y realizar la solicitud de servicio web en segundo plano. Como viewModelScope pertenece a ViewModel, la solicitud continúa incluso si la app pasa por un cambio de configuración.

  1. En el archivo MarsApiService.kt, haz que getPhotos() sea una función de suspensión para que sea asíncrona y no bloquee el subproceso de llamada. Llamas a esta función desde un viewModelScope.
@GET("photos")
suspend fun getPhotos(): String
  1. Abre el archivo ui/screens/MarsViewModel.kt. Desplázate hacia abajo hasta el método getMarsPhotos(). Borra la línea que establece la respuesta de estado en "Set the Mars API Response here!" para que el método getMarsPhotos() esté vacío.
private fun getMarsPhotos() {}
  1. Dentro de getMarsPhotos(), inicia la corrutina mediante viewModelScope.launch.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

private fun getMarsPhotos() {
    viewModelScope.launch {}
}
  1. Dentro de viewModelScope, usa el objeto singleton MarsApi para llamar al método getPhotos() desde la interfaz retrofitService. Guarda la respuesta que se muestra en un val llamado listResult.
import com.example.marsphotos.network.MarsApi

viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}
  1. Asigna el resultado que acabamos de recibir del servidor backend a marsUiState marsUiState es un objeto de estado mutable que representa el estado de la solicitud web más reciente.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
  1. Ejecuta la app y fíjate que se cierra de inmediato y puede mostrar o no una ventana emergente de error. Esta es una falla.
  2. En Android Studio, haz clic en la pestaña Logcat y observa el error en el registro, que comienza con una línea como la siguiente: "------- beginning of crash".
    --------- beginning of crash
22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.example.android.marsphotos, PID: 22803
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)
...

Este mensaje de error indica que a la app le podrían faltar los permisos INTERNET. En la siguiente tarea, se describe cómo agregar permisos de Internet a la app y resolver este problema.

7. Agrega permisos de Internet y manejo de excepciones

Permisos de Android

El objetivo de los permisos de Android es proteger la privacidad de un usuario de Android. Las apps para Android deben declarar o solicitar permisos a fin de acceder a datos sensibles del usuario, como contactos, registros de llamadas y ciertas funciones del sistema, como la cámara o Internet.

Para que tu app pueda acceder a Internet, necesita el permiso INTERNET. La conexión a Internet presenta problemas de seguridad, por lo que las apps no tienen conexión a Internet de forma predeterminada. Debes declarar explícitamente que la aplicación necesita acceso a Internet. Esta declaración se considera un permiso normal. Para obtener más información sobre los permisos de Android y sus tipos, consulta Permisos en Android.

En este paso, tu app declara los permisos que requiere incluyendo etiquetas <uses-permission> en el archivo AndroidManifest.xml.

  1. Abre manifests/AndroidManifest.xml. Agrega esta línea justo antes de la etiqueta <application>:
<uses-permission android:name="android.permission.INTERNET" />
  1. Vuelve a compilar y ejecutar la app.

Si tienes una conexión a Internet activa, verás el texto JSON con datos relacionados a las fotos de Marte. Observa cómo se repiten id y img_src para cada registro de imagen. Obtendrás más información sobre el formato JSON más adelante en el codelab.

b82ddb79eff61995.png

  1. Presiona el botón Atrás en el dispositivo o emulador para cerrar la app.

Manejo de excepciones

Hay un error en tu código. Para verlo, sigue estos pasos:

  1. Activa el modo de avión en tu dispositivo o emulador para simular un error de conexión de red.
  2. Vuelve a abrir la app desde el menú Recientes o reiníciala desde Android Studio.
  3. Haz clic en la pestaña Logcat en Android Studio y observa la excepción fatal en el registro, que es similar a la siguiente:
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.android.marsphotos, PID: 3302

Este mensaje de error indica que la aplicación intentó conectarse y se agotó el tiempo de espera. Este tipo de excepciones son muy comunes en tiempo real. A diferencia del problema de permisos, este error no es algo que puedas solucionar, pero puedes manejarlo. En el siguiente paso, aprenderás a manejar esas excepciones.

Excepciones

Las excepciones son errores que pueden ocurrir durante el tiempo de ejecución (no durante el tiempo de compilación) y finalizar la app de manera repentina sin notificar al usuario. Esto puede afectar la experiencia del usuario. El manejo de excepciones es un mecanismo mediante el cual puedes evitar que la app se cierre de forma repentina y manejar el problema de una forma sencilla.

El motivo de las excepciones puede ser tan simple como la división por cero o un error con la conexión de red. Estas excepciones son similares a la IllegalArgumentException que se analiza en un codelab anterior.

Estos son algunos ejemplos de problemas que podrían presentarse durante la conexión a un servidor:

  • La URL o el URI que se usaron en la API son incorrectos
  • El servidor no está disponible y la app no pudo conectarse a él
  • Un problema de latencia de la red
  • Conexión a Internet baja o nula en el dispositivo

No se pueden controlar estas excepciones durante el tiempo de compilación, pero puedes usar un bloque try-catch para manejar la excepción en el tiempo de ejecución. Para obtener más información, consulta Excepciones.

Sintaxis de ejemplo para el bloque try-catch

try {
    // some code that can cause an exception.
}
catch (e: SomeException) {
    // handle the exception to avoid abrupt termination.
}

Dentro del bloque try, agrega el código en el que prevés una excepción. En tu app, esta es una llamada de red. En el bloque catch, debes implementar el código que impide la finalización abrupta de la app. Si hay una excepción, se ejecutará el bloque catch para que esta se recupere del error, en lugar de cerrarse de manera repentina.

  1. En getMarsPhotos(), dentro del bloque launch, agrega un bloque try alrededor de la llamada MarsApi para controlar las excepciones.
  2. Agrega un bloque catch después del bloque try.
import java.io.IOException

viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       marsUiState = listResult
   } catch (e: IOException) {

   }
}
  1. Ejecuta la app una vez más. Observa que la app no falla esta vez.

Agrega IU de estado

En la clase MarsViewModel, el estado más reciente de la solicitud web, marsUiState se guarda como un objeto de estado mutable. Sin embargo, esta clase carece de la capacidad de guardar el estado diferente: loading, success y failure (cargando, éxito y error).

  • El estado Loading indica que la app está esperando datos.
  • El estado Success indica que los datos se recuperaron correctamente del servicio web.
  • El estado Error indica cualquier error de red o conexión.

Para representar estos tres estados en tu aplicación, debes usar una interfaz sellada. Un sealed interface facilita la administración del estado, ya que limita los valores posibles. En la app de Mars, restringes la respuesta web marsUiState a tres estados (objetos de clase de datos): loading, success y error, lo que se parece al siguiente código:

// No need to copy over
sealed interface MarsUiState {
   data class Success : MarsUiState
   data class Loading : MarsUiState
   data class Error : MarsUiState
}

En el fragmento de código anterior, en el caso de una respuesta exitosa, recibirás información sobre fotos de Marte del servidor. Para almacenar los datos, agrega un parámetro de constructor a la clase de datos Success.

En el caso de los estados Loading y Error, no es necesario establecer datos nuevos ni crear otros objetos; sólo se pasa la respuesta web. Cambia la clase data a Object a fin de crear los objetos para las respuestas web.

  1. Abre el archivo ui/MarsViewModel.kt. Después de las sentencias de importación, agrega la interfaz sellada MarsUiState. Esta adición hace que los valores del objeto MarsUiState puedan ser exhaustivos.
sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}
  1. Dentro de la clase MarsViewModel, actualiza la definición marsUiState. Cambia el tipo a MarsUiState y MarsUiState.Loading como valor predeterminado. Haz que el método set sea privado para proteger las escrituras en marsUiState.
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
  private set
  1. Desplázate hacia abajo hasta el método getMarsPhotos(). Actualiza el valor de marsUiState a MarsUiState.Success y pasa listResult.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
  1. Dentro del bloque catch, controla la respuesta de falla. Establece MarsUiState en Error.
catch (e: IOException) {
   marsUiState = MarsUiState.Error
}
  1. Puedes quitar la asignación marsUiState del bloque try-catch. La función completada debería verse como el siguiente código:
private fun getMarsPhotos() {
   viewModelScope.launch {
       marsUiState = try {
           val listResult = MarsApi.retrofitService.getPhotos()
           MarsUiState.Success(listResult)
       } catch (e: IOException) {
           MarsUiState.Error
       }
   }
}
  1. En el archivo screens/HomeScreen.kt, agrega una expresión when en marsUiState. Si el marsUiState es MarsUiState.Success, llama a ResultScreen y pasa marsUiState.photos. Ignora los errores por ahora.
import androidx.compose.foundation.layout.fillMaxWidth

fun HomeScreen(
   marsUiState: MarsUiState,
   modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )
    }
}
  1. Dentro del bloque when, agrega verificaciones para MarsUiState.Loading y MarsUiState.Error. Haz que la app muestre los elementos componibles LoadingScreen, ResultScreen y ErrorScreen, que implementarás más adelante.
import androidx.compose.foundation.layout.fillMaxSize

fun HomeScreen(
   marsUiState: MarsUiState,
   modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )

        is MarsUiState.Error -> ErrorScreen( modifier = modifier.fillMaxSize())
    }
}
  1. Abre res/drawable/loading_animation.xml. Este elemento de diseño es una animación que rota un elemento de diseño de imagen, loading_img.xml, alrededor del punto central (no ves la animación en la vista previa).

92a448fa23b6d1df.png

  1. En el archivo screens/HomeScreen.kt, debajo del elemento de componibilidad HomeScreen, agrega la siguiente función de componibilidad LoadingScreen para mostrar la animación de carga. El recurso de elemento de diseño loading_img se incluye en el código de partida.
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.Image

@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
    Image(
        modifier = modifier.size(200.dp),
        painter = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.loading)
    )
}
  1. Debajo del elemento de componibilidad LoadingScreen, agrega la siguiente función de componibilidad ErrorScreen para que la app pueda mostrar el mensaje de error.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding

@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
        )
        Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
    }
}
  1. Vuelve a ejecutar la app con el modo de avión activado. No se cierra de forma abrupta esta vez y muestra el siguiente mensaje de error:

28ba37928e0a9334.png

  1. Desactiva el modo de avión en tu teléfono o emulador. Ejecuta y prueba tu app para asegurarte de que todo funcione correctamente y puedas ver la cadena JSON.

8. Analiza la respuesta JSON con kotlinx.serialization

JSON

Por lo general, los datos solicitados tienen un formato de datos común, como XML o JSON. Cada llamada muestra datos estructurados y tu app debe saber cuál es esa estructura para poder leer los datos de la respuesta.

Por ejemplo, en esta app, recuperarás los datos del servidor https://android-kotlin-fun-mars-server.appspot.com/photos. Cuando ingresas esta URL en el navegador, aparece una lista de IDs y URLs de imágenes de la superficie de Marte en formato JSON.

Estructura de la respuesta JSON de muestra

se muestran valores clave y objeto JSON

La estructura de una respuesta JSON tiene las siguientes características:

  • La respuesta JSON es un array, que se indica con los corchetes. El array contiene objetos JSON.
  • Los objetos JSON aparecen entre llaves.
  • Cada objeto JSON contiene un conjunto de pares clave-valor separados por comas.
  • Los dos puntos separan la clave y el valor de un par.
  • Los nombres aparecen entre comillas.
  • Los valores pueden ser números, strings, un valor booleano, un array, un objeto (JSON) o nulo.

Por ejemplo, img_src es una URL, que es una string. Cuando pegas la URL en un navegador web, ves una imagen de la superficie de Marte.

b4f9f196c64f02c3.png

En tu app, ahora obtienes una respuesta JSON del servicio web de Marte, lo cual es un excelente comienzo. Pero lo que realmente necesitas para mostrar las imágenes son objetos de Kotlin, no una string JSON grande. Este proceso se denomina deserialización.

La serialización es el proceso de convertir los datos utilizados por una aplicación a un formato que se pueda transferir a través de una red. A diferencia de la serialización, la deserialización es el proceso de leer datos de una fuente externa (como un servidor) y convertirlos en un objeto de entorno de ejecución. Ambos son componentes esenciales de la mayoría de las aplicaciones que intercambian datos por la red.

kotlinx.serialization proporciona conjuntos de bibliotecas que convierten una string JSON en objetos Kotlin. Existe una biblioteca de terceros desarrollada por la comunidad que funciona con Retrofit: Kotlin Serialization Converter.

En esta tarea, usarás la biblioteca kotlinx.serialization para analizar la respuesta JSON del servicio web en objetos de Kotlin útiles que representen fotos de Marte. Cambiarás la app de modo que en lugar de mostrar el JSON sin procesar, muestre la cantidad de fotos de Marte que se muestran.

Cómo agregar dependencias de bibliotecas kotlinx.serialization

  1. Abre build.gradle.kts (Module :app).
  2. En el bloque plugins, agrega el complemento kotlinx serialization.
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
  1. En la sección de dependencies, agrega el siguiente código para incluir la dependencia kotlinx.serialization. Esta dependencia proporciona serialización JSON para los proyectos de Kotlin.
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
  1. Busca las líneas del conversor escalar de Retrofit en el bloque dependencies y cámbialo para usar kotlinx-serialization-converter:

Reemplaza el siguiente código:

// Retrofit with scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

por estee:

// Retrofit with Kotlin serialization Converter

implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
  1. Haz clic en Sync Now para volver a compilar el proyecto con las dependencias nuevas.

Cómo implementar la clase de datos de Mars Photos

Tal como viste anteriormente, una entrada de muestra de la respuesta JSON que obtienes del servicio web es similar a lo que se muestra a continuación:

[
    {
        "id":"424906",
        "img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
    },
...]

En el ejemplo anterior, observa que cada entrada de foto de Marte tiene los siguientes pares clave-valor y clave JSON:

  • id: Es el ID de la propiedad, como una cadena Como se encierra entre comillas (" "), es del tipo String, no Integer.
  • img_src: Es la URL de la imagen como una string.

kotlinx.serialization analiza estos datos JSON y los convierte en objetos Kotlin. Para hacerlo, kotlinx.serialization debe tener una clase de datos Kotlin a fin de almacenar los resultados analizados. En este paso, crearás la clase de datos MarsPhoto.

  1. Haz clic con el botón derecho en el paquete network y selecciona New > Kotlin File/Class.
  2. En la ventana emergente, selecciona Class y, luego, ingresa MarsPhoto como el nombre de la clase. Esto crea un archivo nuevo llamado MarsPhoto.kt en el paquete network.
  3. Para hacer que MarsPhoto sea una clase de datos, agrega la palabra clave data antes de la definición de la clase.
  4. Cambia las llaves {} por paréntesis (). Este cambio genera un error porque las clases de datos deben tener al menos una propiedad definida.
data class MarsPhoto()
  1. Agrega las siguientes propiedades a la definición de la clase MarsPhoto.
data class MarsPhoto(
    val id: String,  val img_src: String
)
  1. Para que la clase MarsPhoto sea serializable, puedes anotarla con @Serializable.
import kotlinx.serialization.Serializable

@Serializable
data class MarsPhoto(
    val id: String,  val img_src: String
)

Observa que cada una de las variables de la clase MarsPhoto corresponde a un nombre de clave en el objeto JSON. Para hacer coincidir los tipos en nuestra respuesta JSON específica, usas objetos String para todos los valores.

Cuando kotlinx serialization analice el JSON, buscará las claves por nombre y completará los objetos de datos con los valores correspondientes.

Anotación @SerialName

A veces, los nombres de claves en una respuesta JSON pueden hacer que las propiedades de Kotlin sean confusas o no coincidan con el estilo de programación recomendado. Por ejemplo, en el archivo JSON, la clave img_src usa un guion bajo, mientras que la convención de Kotlin para las propiedades usa letras mayúsculas y minúsculas (mayúsculas mediales).

Para usar nombres de variables en tu clase de datos que difieren de los nombres de clave en la respuesta JSON, usa la anotación @SerialName. En el siguiente ejemplo, el nombre de la variable en la clase de datos es imgSrc. La variable puede asignarse al atributo JSON img_src usando @SerialName(value = "img_src").

  1. Reemplaza la línea para la clave img_src con la línea que se muestra a continuación.
import kotlinx.serialization.SerialName

@SerialName(value = "img_src")
val imgSrc: String

Actualiza MarsApiService y MarsViewModel

En esta tarea, usarás el conversor kotlinx.serialization para convertir el objeto JSON en objetos Kotlin.

  1. Abre network/MarsApiService.kt.
  2. Observa los errores de referencia no resueltos para ScalarsConverterFactory. Estos errores son el resultado del cambio de dependencia de Retrofit en una sección anterior.
  3. Borra la importación de ScalarConverterFactory. Corregirás el otro error más tarde.

Quita lo siguiente:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. En la declaración del objeto retrofit, cambia el compilador de Retrofit para usar kotlinx.serialization en lugar de ScalarConverterFactory.
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType

private val retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(BASE_URL)
        .build()

Ahora que kotlinx.serialization está implementado, puedes pedirle a Retrofit que muestre una lista de objetos MarsPhoto del array JSON en lugar de mostrar una cadena JSON.

  1. Actualiza la interfaz MarsApiService para que Retrofit muestre una lista de objetos MarsPhoto, en lugar de mostrar una String.
interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}
  1. Realiza cambios similares en el viewModel. Abre MarsViewModel.kt y desplázate hacia abajo hasta el método getMarsPhotos().

En el método getMarsPhotos(), listResult es List<MarsPhoto> y ya no es String. El tamaño de la lista es la cantidad de fotos que se recibieron y analizaron.

  1. Para imprimir la cantidad de fotos recuperadas, actualiza marsUiState de la siguiente manera:
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
   "Success: ${listResult.size} Mars photos retrieved"
)
  1. Asegúrate de que el modo de avión esté desactivado en el dispositivo o emulador. Compila y ejecuta la app.

Esta vez, el mensaje debería mostrar la cantidad de propiedades que muestra el servicio web y no una cadena JSON grande:

a59e55909b6e9213.png

9. Código de solución

Para descargar el código del codelab terminado, puedes usar estos comandos de git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout repo-starter

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución para este codelab, míralo en GitHub.

10. Resumen

Servicios web de REST

  • Un servicio web es una funcionalidad basada en software que se ofrece a través de Internet y que permite que tu app haga solicitudes y recupere datos.
  • Los servicios web comunes usan una arquitectura REST. Los servicios web que ofrecen arquitectura REST se conocen como servicios RESTful. Los servicios web RESTful se crean con componentes y protocolos web estándar.
  • Realizas una solicitud a un servicio web REST de manera estandarizada mediante URIs.
  • Para usar un servicio web, una app debe establecer una conexión de red y comunicarse con el servicio. Luego, la app debe recibir y analizar los datos de respuesta a un formato que pueda usar la app.
  • La biblioteca Retrofit es una biblioteca cliente que permite a tu app realizar solicitudes a un servicio web REST.
  • Usa conversores para indicarle a Retrofit qué hacer con los datos que envía al servicio web y recupera de él. Por ejemplo, el conversor de ScalarsConverter trata los datos del servicio web como String o cualquier otra primitiva.
  • Para permitir que tu app establezca conexiones a Internet, agrega el permiso "android.permission.INTERNET" en el manifiesto de Android.
  • La inicialización diferida delega la creación de un objeto a la primera vez que se usa. Crea la referencia, pero no el objeto. Cuando se accede a un objeto por primera vez, se crea una referencia que se usará cada vez a partir de ese momento.

Análisis de JSON

  • La respuesta de un servicio web suele tener el formato JSON, un formato común para representar datos estructurados.
  • Un objeto JSON es una colección de pares clave-valor.
  • Una colección de objetos JSON es un array JSON. Obtienes un array JSON como una respuesta de un servicio web.
  • Las claves en un par clave-valor están entre comillas. Los valores pueden ser números o strings.
  • En Kotlin, las herramientas de serialización de datos están disponibles en un componente separado, kotlinx.serialization. kotlinx.serialization proporciona conjuntos de bibliotecas que convierten una cadena JSON en objetos Kotlin.
  • Existe una biblioteca de Kotlin Serialization Converter desarrollada por la comunidad para Retrofit: retrofit2-kotlinx-serialization-converter.
  • kotlinx.serialization hace coincidir las claves en una respuesta JSON con propiedades en un objeto de datos que tienen el mismo nombre.
  • A fin de usar un nombre de propiedad diferente para una clave, anota esa propiedad con la anotación @SerialName y el nombre de la clave JSON value.

11. Más información

Documentación para desarrolladores de Android:

Documentación de Kotlin:

Otra opción: