Cómo obtener datos de Internet

La mayoría de las apps de Android en el mercado se conectan a Internet para realizar algunas operaciones de red, como recuperar mensajes de correo electrónico, mensajes o información similar de un servidor backend. Gmail, YouTube y Google Fotos son algunas apps de ejemplo que se conectan a Internet para mostrar los datos del usuario.

En este codelab, usarás bibliotecas desarrolladas de código abierto para compilar la capa de red y obtener datos de un servidor backend. Esto simplifica enormemente la obtención de datos y también ayuda a la app a cumplir con las prácticas recomendadas de Android, como la realización de las operaciones en un subproceso en segundo plano. También actualizarás la interfaz de usuario de la app si la conexión a Internet es lenta o no está disponible. Esto mantendrá al usuario informado de cualquier problema de conectividad de red.

Conocimientos que ya deberías tener

  • Cómo crear y usar fragmentos
  • Cómo usar los componentes de la arquitectura de Android ViewModel y LiveData
  • Cómo agregar dependencias en un archivo de Gradle

Qué aprenderás

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

Actividades

  • Modificarás una app de inicio para realizar una solicitud a la API de servicio web y manejar la respuesta.
  • Implementarás una capa de red para tu app usando la biblioteca Retrofit.
  • Analizarás la respuesta JSON del servicio web en los objetos LiveData de tu app con la biblioteca Moshi.
  • Usarás la compatibilidad de Retrofit para las corrutinas a fin de simplificar el código.

Requisitos

  • Una computadora que tenga Android Studio instalado
  • Código inicial para la app de MarsPhotos

En esta ruta de aprendizaje, trabajarás con una app de inicio llamada MarsPhotos, que muestra imágenes de la superficie de Marte. Esta app se conecta a un servicio web para recuperar y mostrar las fotos de Marte. Las imágenes son fotografías reales de Marte capturadas por los Mars rovers de la NASA. A continuación, se muestra la captura de pantalla de la app final, que contiene una cuadrícula de imágenes de miniaturas de propiedades compilada con un RecyclerView.

6c26142b52c51285.png

La versión de la app que compilas en este codelab no tendrá muchos elementos visuales llamativos, ya que se centra en la parte de la capa de red de la app para conectarse a Internet y descargar los datos de propiedad sin procesar con un servicio web. Para garantizar que los datos se recuperen y analicen correctamente, solo mostrarás el número de fotos recibidas del servidor backend en una vista de texto:

e98a0641540fcb73.png

Descarga el código de inicio

Este codelab te brinda el código de inicio para que lo extiendas con funciones que se explican en este codelab. Es posible que el código de inicio incluya código que te resulte conocido y desconocido por los codelabs anteriores. Aprenderás más sobre el código desconocido en codelabs posteriores.

Si usas el código de inicio de GitHub, ten en cuenta que el nombre de la carpeta es android-basics-kotlin-mars-photos-app. Selecciona esta carpeta cuando abras el proyecto en Android Studio.

A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:

Obtén el código

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.

Ejecuta el código de inicio

  1. Abre el proyecto descargado en Android Studio. El nombre de la carpeta del proyecto es android-basics-kotlin-mars-photos-app. La estructura de carpetas del código de inicio debería verse como se muestra más abajo.
  2. En el panel de Android, expande app > java. Observa que la app tiene una carpeta de paquete llamada overview. Esta es la capa de la IU de la app.

39027f91862361f1.png

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

4886b6b36023a53f.png

  1. Explora los archivos para comprender el código de inicio. Para los archivos de diseño, puedes usar la opción Split en la esquina superior derecha a fin de obtener una vista previa del diseño y el XML al mismo tiempo.

Explicación del código de inicio

En esta tarea, te familiarizarás con la estructura del proyecto. A continuación, se explican los archivos y las carpetas importantes del proyecto.

OverviewFragment:

  • Este es el fragmento que se muestra dentro de MainActivity. El texto del marcador de posición que viste en el paso anterior se muestra en este fragmento.
  • En el siguiente codelab, este fragmento mostrará los datos recibidos del servidor backend de fotos de Marte.
  • Esta clase contiene una referencia al objeto OverviewViewModel.
  • OverviewFragment tiene una función onCreateView() que aumenta el diseño de fragment_overview con la vinculación de datos. Luego, establece el propietario del ciclo de vida para la vinculación y establece la variable viewModel en el objeto de vinculación.
  • Como el propietario del ciclo de vida está asignado, se buscarán cambios automáticamente en cualquier LiveData utilizado en la vinculación de datos, y la IU se actualizará según corresponda.

OverviewViewModel:

  • Este es el modelo de vista correspondiente para OverviewFragment.
  • Esta clase contiene una propiedad MutableLiveData llamada _status junto con su propiedad de copia de seguridad. 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 esto para mostrar los datos obtenidos del servidor. El objetivo de este codelab es que actualices status LiveData dentro de ViewModel con datos reales que obtuviste de Internet.

res/layout/fragment_overview.xml:

  • Este diseño se configuró para usar la vinculación de datos y consiste en una sola TextView.
  • Declara una variable OverviewViewModel y, luego, vincula el status de ViewModel a TextView.

MainActivity.kt: La única tarea de esta actividad es cargar el diseño de la actividad, activity_main.

layout/activity_main.xml: es el diseño principal de la actividad con un solo FragmentContainerView que apunta a fragment_overview. Se creará una instancia del fragmento de descripción general cuando se inicie la app.

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

d5a05ab8fd5ff011.png

OverviewViewModel es responsable de realizar la llamada de red para obtener los datos de las fotos de Marte. En ViewModel, usas LiveData con vinculación de datos optimizada para ciclos de vida a fin de actualizar la IU de la app cuando cambian los datos.

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.

b3ab0ee52bfd791e.png

4a23a1ba3307b2a5.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.

Se realizan solicitudes a los servicios web RESTful de manera estandarizada a través de URI. Un URI (identificador uniforme de recursos) identifica un recurso en el servidor por nombre, sin indicar su ubicación ni cómo acceder a él. Por ejemplo, en la app de esta lección, recuperas las URL de la imagen usando el siguiente URI del servidor (este servidor aloja las fotos y los inmuebles de Marte):

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

Un localizador uniforme de recursos (URL) es un URI que especifica los medios para modificar u obtener la representación de un recurso, es decir, especifica su mecanismo de acceso principal y la ubicación de la red.

Por ejemplo:

La siguiente URL obtiene una lista de todas 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 URL hacen referencia a un recurso identificado como /realestate o /photos, que se puede obtener a través del Protocolo de transferencia de hipertexto (http:) de la red. En este codelab, usarás el extremo /photos.

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 o PUT para agregar datos nuevos al servidor, crearlos o actualizarlos.
  • DELETE para borrar datos del servidor

Tu app realizará una solicitud GET HTTP al servidor para la información de fotos de Marte y, luego, el servidor mostrará una respuesta a nuestra app que incluye URL de imágenes.

9fb57e255df97a4d.png

6da405d572445df9.png

La respuesta de un servicio web suele tener uno de los formatos web comunes, como XML o JSON, para representar datos estructurados en pares clave-valor. Obtendrás más información sobre JSON en la siguiente tarea.

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 API de Android principales. Son, en su mayoría, de código abierto, y están desarrolladas por la comunidad y mantenidas por las contribuciones colectivas de la enorme comunidad de Android de todo el mundo. Esto permite a los desarrolladores de Android como tú 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 saberlo, en su página de GitHub, consulta los problemas abiertos (algunos de ellos son solicitudes de funciones) y los problemas cerrados. Si los desarrolladores resuelven los problemas y responden a las solicitudes de funciones con frecuencia, esto implica que la biblioteca tiene mantenimiento regular y es una buena candidata para usarla en la app. También tienen una página de documentación de Retrofit.

La biblioteca Retrofit se comunicará con el backend. Crea URI para el servicio web según los parámetros que le pasamos. Verás más información al respecto en secciones posteriores.

c9e1034e86327abd.png

Cómo agregar dependencias de Retrofit

Gradle de Android te permite agregar bibliotecas externas a tu proyecto. Además de la dependencia de biblioteca, también debes incluir el repositorio en el que se aloja la biblioteca. Las bibliotecas de Google como ViewModel y LiveData de la biblioteca Jetpack se alojan en el repositorio de Google. La mayoría de las bibliotecas de la comunidad se realizan en JCenter, como Retrofit.

  1. Abre el archivo de nivel superior build.gradle(Project: MarsPhotos) del proyecto. Observa los repositorios que aparecen debajo del bloque repositories. Deberías ver dos repositorios: google() y jcenter().
repositories {
   google()
   jcenter()
}
  1. Abre el archivo de Gradle de nivel de módulo, build.gradle (Module: MarsPhots.app).
  2. En la sección dependencies, agrega estas líneas para las bibliotecas Retrofit:
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

La primera dependencia es para la biblioteca Retrofit2 y la segunda es para el conversor escalar de Retrofit. Este conversor permite que Retrofit muestre el resultado JSON como String. Las dos bibliotecas funcionan juntas.

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

Cómo agregar compatibilidad con funciones del lenguaje Java 8

Muchas bibliotecas de terceros, como Retrofit2, usan funciones del lenguaje Java 8. El complemento de Android para Gradle tiene compatibilidad integrada para usar determinadas funciones del lenguaje Java 8.

  1. Para usar las funciones integradas, necesitas el siguiente código en el archivo build.gradle de tu módulo. Ya completaste este paso. Asegúrate de que el siguiente código esté presente en tu build.gradle(Module: MarsPhotos.app).
android {
  ...

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = '1.8'
  }
}

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 TextView mostrará la string de respuesta JSON que aparece 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.

64fe0b5717a31f71.png

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

  • Crea una capa de red: la clase MarsApiService.
  • Crea un objeto de Retrofit con la URL de base y la fábrica del conversor.
  • Crear una interfaz que explique cómo habla Retrofit con nuestro 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. Crea un paquete nuevo llamado "network". En el panel de tu proyecto de Android, haz clic con el botón derecho en el paquete, com.example.android.marsphotos. Selecciona New > Package. En la ventana emergente, agrega network al final del nombre del paquete sugerido.
  2. Crea un nuevo archivo de Kotlin debajo del nuevo paquete network. Asígnale el nombre MarsApiService..
  3. Abre network/MarsApiService.kt. Agrega la siguiente constante para la URL de base del servicio web.
private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. Justo debajo de esa constante, agrega un compilador de Retrofit para compilar y crear un objeto Retrofit.
private val retrofit = Retrofit.Builder()

Importa retrofit2.Retrofit cuando se te solicite.

  1. 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 strings y otros tipos primitivos, por lo que llamas a addConverterFactory() en el compilador con una instancia de ScalarsConverterFactory.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())

Importa retrofit2.converter.scalars.ScalarsConverterFactory cuando se te solicite.

  1. Agrega el URI de base para el servicio web con el método baseUrl(). 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. Dentro de la interfaz MarsApiService, agrega una función llamada getPhotos() 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 se llama photos. Como se mencionó en la tarea anterior, usarás el extremo /photos en este codelab.
interface MarsApiService {
    @GET("photos")
    fun getPhotos()
}

Importa retrofit2.http.GET cuando se solicite.

  1. Cuando se invoca el método getPhotos(), Retrofit agrega el extremo photos a la URL de base (que definiste en el compilador de Retrofit) que se usó para iniciar la solicitud. 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, tiene un punto de acceso global a ese objeto. La inicialización de la declaración del objeto es segura para los subprocesos y se realiza durante el primer acceso.

Kotlin facilita la declaración de singleton. 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:

// Object declaration
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }
​
    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

// To refer to the object, use its name directly.
DataProviderManager.registerDataProvider(...)

La función call to create() en un objeto Retrofit es costosa, y la app necesita solo una instancia del servicio de la API de Retrofit. Por lo tanto, expones el servicio al resto de la app con 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 se puede acceder desde 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. Realizas esta inicialización diferida para asegurarte de que se inicialice en su primer uso. Solucionarás el error en los pasos siguientes.
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 llama a MarsApi.retrofitService, el emisor tendrá el mismo acceso a un 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 OverviewViewModel

En este paso, implementarás un método getMarsPhotos() que llama al servicio de actualización y, luego, maneja la string 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.

Usarás ViewModelScope para iniciar la corrutina y realizar la llamada de red Retrofit en segundo plano.

  1. En MarsApiService, haz que getPhotos() sea una función de suspensión. Así que puedes llamar a este método desde una corrutina.
@GET("photos")
suspend fun getPhotos(): String
  1. Abre overview/OverviewViewModel. 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!".. El método getMarsPhotos() debería estar vacío ahora.
private fun getMarsPhotos() {

}
  1. Dentro de getMarsPhotos(), inicia la corrutina mediante viewModelScope.launch.
private fun getMarsPhotos() {
    viewModelScope.launch {
    }
}

Importa androidx.lifecycle.viewModelScope y kotlinx.coroutines.launch cuando se te solicite.

  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.
viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}

Importa com.example.android.marsphotos.network.MarsApi cuando se te solicite.

  1. Asigna el resultado que acabamos de recibir del servidor backend a _status.value.
 val listResult = MarsApi.retrofitService.getProperties()
 _status.value = listResult
  1. Ejecuta la app, ten en cuenta que la app se cierra de inmediato y puede mostrar una ventana emergente de error o no.
  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. Para solucionar este problema, agrega permisos de Internet a la app en la próxima tarea.

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. Esto se considera un permiso normal. Para obtener más información sobre los permisos de Android y sus tipos, consulta la documentación.

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

  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, deberías ver el texto JSON con datos relacionados a las fotos de Marte. Obtendrás más información sobre el formato JSON más adelante en el codelab.

f7ba3feaf864d4cf.png

  1. Presiona el botón Atrás en el dispositivo o emulador para cerrar la app.
  2. Activa el modo de avión en tu dispositivo o emulador para simular un error de conexión de red. Vuelve a abrir la app desde el menú Recientes o reinicia la app 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
    java.net.SocketTimeoutException: timeout
...

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. En el siguiente paso, aprenderás a manejar esas excepciones.

Manejo de 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 podría ser tan simple como la división por cero o un error en la red. Estas excepciones son similares a la NumberFormatException que aprendiste en un codelab anterior.

Ejemplos de posibles problemas 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.
  • Hay un problema en la latencia de la red.
  • Conexión a Internet baja o nula en el dispositivo.

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

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, realizarás el código donde preves una excepción. En tu app, sería una llamada de red. En el bloque catch, implementarás el código que impide la finalización repentina de la app. Si existe una excepción, se ejecutará el bloque catch para recuperarse del error en lugar de cerrar la app de manera repentina.

  1. Abre overview/OverviewViewModel.kt. Desplázate hacia abajo hasta el método getMarsPhotos(). Dentro del bloque de lanzamiento, agrega un bloque try alrededor de la llamada MarsApi para manejar las excepciones. Agrega el bloque catch después del bloque try:
viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       _status.value = listResult
   } catch (e: Exception) {

   }
}
  1. Dentro del bloque catch {}, controla la respuesta de falla. Para mostrar el mensaje de error al usuario, configura e.message como _status.value.
catch (e: Exception) {
   _status.value = "Failure: ${e.message}"
}
  1. Vuelve a ejecutar la app con el modo de avión activado. La app no se cierra de forma repentina, pero muestra un mensaje de error.

2fbc318b4fff2f34.png

  1. Desactiva el modo de avión en tu teléfono o emulador. Ejecuta y prueba tu app, asegúrate de que todo funcione bien y que puedas ver la string JSON.

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 de este servidor: https:// android-kotlin-fun-mars-server.appspot.com/photos. Si ingresas esta URL en el navegador,

verás una lista de los ID y las URL de imagen de la superficie de Marte en un formato JSON.

Estructura de la respuesta JSON de muestra:

68fdfa54410ee03e.png

  • 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 nombre-valor. El nombre y el valor están separados por dos puntos.
  • 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. Si pegas la URL en un navegador web, verás una imagen de la superficie de Marte.

17116bdeb21fec0d.png

Ahora obtienes una respuesta JSON del servicio web de Marte, que es un buen comienzo. Pero lo que necesitas son objetos Kotlin, no una string JSON grande. Hay una biblioteca externa llamada Moshi, que es un analizador de JSON de Android que convierte una string JSON en objetos Kotlin. Retrofit tiene un conversor que funciona con Moshi, por lo que es una excelente biblioteca para sus propósitos aquí.

En esta tarea, usarás la biblioteca Moshi con Retrofit 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.

Agrega dependencias de la biblioteca Moshi

  1. Abre build.gradle (Module: app).
  2. En la sección de dependencias, agrega el código que se muestra a continuación para incluir la dependencia de Moshi. Esta dependencia agrega compatibilidad con la biblioteca JSON de Moshi compatible con Kotlin.
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
  1. Busca las líneas del conversor de escalar Retrofit en el bloque dependencies y cambia estas dependencias para usar converter-moshi:

Reemplaza lo siguiente

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

De esta forma

// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.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

Una entrada de muestra de la respuesta JSON que obtienes del servicio web se ve de la siguiente manera, similar a lo que viste antes:

[{
    "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 estos pares clave-valor y valores JSON:

  • id: El ID de la propiedad, como una string. Como se encierra en " ", es del tipo String, no Integer.
  • img_src: La URL de la imagen como una string.

Moshi analiza estos datos JSON y los convierte en objetos Kotlin. Para ello, Moshi necesita tener una clase de datos de Kotlin a fin de almacenar los resultados analizados, por lo que 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. Cambia las llaves {} por paréntesis (). Esto deja un error, ya que 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
)

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 Moshi analiza el JSON, busca las claves por nombre y completa los objetos de datos con los valores correspondientes.

Anotación de @Json

A veces, los nombres de clave en una respuesta JSON pueden hacer que las propiedades de Kotlin sean confusas o no coincidan con el estilo de codificació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.

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 @Json. En este ejemplo, el nombre de la variable en la clase de datos es imgSrcUrl. La variable puede asignarse al atributo JSON img_src usando @Json(name = "img_src").

  1. Reemplaza la línea para la clave img_src con la línea que se muestra a continuación. Importa com.squareup.moshi.Json cuando se solicite.
@Json(name = "img_src") val imgSrcUrl: String

Cómo actualizar MarsApiService y OverviewViewModel

En esta tarea, crearás un objeto de Moshi con el compilador Moshi, similar al compilador Retrofit.

Reemplaza ScalarsConverterFactory por KotlinJsonAdapterFactory para indicarle a Retrofit que puede usar Moshi para convertir la respuesta JSON en objetos Kotlin. Luego, actualizarás la API de la red y ViewModel para usar el objeto Moshi.

  1. Abre network/MarsApiService.kt. Observa los errores de referencia no resueltos para ScalarsConverterFactory. Esto se debe al cambio de dependencia de Retrofit que realizaste en un paso anterior. Borra la importación de ScalarConverterFactory. Pronto corregirás el otro error.

Quitar:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. En la parte superior del archivo, justo antes de la creación de Retrofit, agrega el siguiente código para crear el objeto Moshi similar al objeto Retrofit.
private val moshi = Moshi.Builder()

Importa com.squareup.moshi.Moshi y com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory cuando se te solicite.

  1. Para que las anotaciones de Moshi funcionen correctamente con Kotlin, en el compilador de Moshi, agrega KotlinJsonAdapterFactory y, luego, llama a build().
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()
  1. En la declaración del objeto retrofit, cambia el creador de Retrofit para usar MoshiConverterFactory en lugar de ScalarConverterFactory y pasa la instancia de moshi que acabas de crear.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

Importa retrofit2.converter.moshi.MoshiConverterFactory cuando se solicite.

  1. Ahora que MoshiConverterFactory está implementado, puedes pedirle a Retrofit que muestre una lista de objetos MarsPhoto del array JSON en lugar de mostrar una string JSON. Actualiza la interfaz MarsApiService para que Retrofit muestre una lista de objetos MarsPhoto, en lugar de mostrar String.
interface MarsApiService {
   @GET("photo")
   fun getPhotos(): List<MarsPhoto>
}
  1. Haz cambios similares a viewModel y abre OverviewViewModel.kt. Desplázate hacia abajo hasta el método getMarsPhotos().
  2. En el método getMarsPhotos(), listResult es List<MarsPhoto>, y ya no es un String. El tamaño de la lista es la cantidad de fotos que se recibieron y analizaron. Para imprimir la cantidad de fotos recuperadas, actualiza _status.value de la siguiente manera:
_status.value = "Success: ${listResult.size} Mars photos retrieved"

Importa com.example.android.marsphotos.network.MarsPhoto cuando se te solicite.

  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 se muestran del servicio web, no una string grande en JSON:

7da53c64bd36fe74.png

build.gradle(Module : MarsPhotos.app)

Estas son las dependencias nuevas que se incluirán.

dependencies {
    ...
    // Moshi
    implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'

    // Retrofit with Moshi Converter
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    ...
}

Manifests/AndroidManifest.xml

Agrega el permiso de Internet, código <uses-permission..>, desde el fragmento de código siguiente.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.marsphotos">

    <!-- In order for our app to access the Internet, we need to define this permission. -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ...
    </application>

</manifest>

network/MarsPhoto.kt

package com.example.android.marsphotos.network

import com.squareup.moshi.Json

/**
* This data class defines a Mars photo which includes an ID, and the image URL.
* The property names of this data class are used by Moshi to match the names of values in JSON.
*/
data class MarsPhoto(
   val id: String,
   @Json(name = "img_src") val imgSrcUrl: String
)

network/MarsApiService.kt

package com.example.android.marsphotos.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET

private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"

/**
* Build the Moshi object with Kotlin adapter factory that Retrofit will be using.
*/
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()

/**
* The Retrofit object with the Moshi converter.
*/
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

/**
* A public interface that exposes the [getPhotos] method
*/
interface MarsApiService {
   /**
    * Returns a [List] of [MarsPhoto] and this method can be called from a Coroutine.
    * The @GET annotation indicates that the "photos" endpoint will be requested with the GET
    * HTTP method
    */
   @GET("photos")
   suspend fun getPhotos() : List<MarsPhoto>
}

/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object MarsApi {
   val retrofitService: MarsApiService by lazy { retrofit.create(MarsApiService::class.java) }
}

Overview/OverviewViewModel.kt

package com.example.android.marsphotos.overview

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.marsphotos.network.MarsApi
import kotlinx.coroutines.launch

/**
* The [ViewModel] that is attached to the [OverviewFragment].
*/
class OverviewViewModel : ViewModel() {

   // The internal MutableLiveData that stores the status of the most recent request
   private val _status = MutableLiveData<String>()

   // The external immutable LiveData for the request status
   val status: LiveData<String> = _status
   /**
    * Call getMarsPhotos() on init so we can display status immediately.
    */
   init {
       getMarsPhotos()
   }

   /**
    * Gets Mars photos information from the Mars API Retrofit service and updates the
    * [MarsPhoto] [List] [LiveData].
    */
   private fun getMarsPhotos() {
       viewModelScope.launch {
           try {
               val listResult = MarsApi.retrofitService.getPhotos()
               _status.value = "Success: ${listResult.size} Mars photos retrieved"
           } catch (e: Exception) {
               _status.value = "Failure: ${e.message}"
           }
       }
   }
}

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 URI.
  • 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 aplicación realizar solicitudes a un servicio web REST.
  • Usar conversores para indicarle a Retrofit qué hacer con los datos que envía al servicio web y regresa del servicio web. 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.

Análisis de JSON

  • A menudo, la respuesta de un servicio web tiene 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.
  • La biblioteca Moshi es un analizador de JSON de Android que convierte una string JSON en objetos Kotlin. Retrofit tiene un conversor que funciona con Moshi.
  • Moshi 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 @Json y el nombre de la clave JSON.

Documentación para desarrolladores de Android:

Documentación de Kotlin:

Otro: