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.
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
.
3. Explora la app de partida de Mars Photos
Descarga el código de partida
Para comenzar, descarga el código de partida:
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
- Abre el proyecto descargado en Android Studio. El nombre de la carpeta del proyecto es
basic-android-kotlin-compose-training-mars-photos
. - En el panel de Android, expande app > kotlin + java. Observa que la app tiene una carpeta de paquete llamada
ui
. Esta es la capa de la IU de la app.
- 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.
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
MarsPhotosApp
componible, que muestra el contenido en pantalla, como la barra de la app superior y el elementoHomeScreen
componible. 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
llamadamarsUiState
. 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 elMutableState
dentro deViewModel
con datos reales que obtengas de Internet.
screens\HomeScreen.kt
:
- Este archivo contiene los elementos de componibilidad
HomeScreen
yResultScreen
.ResultScreen
tiene un diseñoBox
simple que muestra el valor demarsUiState
en un elemento de componibilidadText
.
MainActivity.kt
:
- La única tarea de esta actividad es cargar el
ViewModel
y mostrar el elemento de componibilidadMarsPhotosApp
.
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.
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.
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.
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 de backend que ya está escrito. En este codelab, usarás la biblioteca Retrofit, una biblioteca de terceros para comunicarte con el servidor de 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.
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.
- Abre el archivo de Gradle de nivel de módulo
build.gradle.kts (Module :app)
. - 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.
- 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.
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:
- Haz clic con el botón derecho en el paquete com.example.marsphotos del panel de tu proyecto de Android y selecciona New > Package.
- En la ventana emergente, agrega network al final del nombre del paquete sugerido.
- Crea un nuevo archivo de Kotlin debajo del nuevo paquete. Asígnale el nombre
MarsApiService
. - Abre
network/MarsApiService.kt
. - Agrega la siguiente constante para la URL base del servicio web.
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- 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.
- Llama a
addConverterFactory()
en el compilador con una instancia deScalarsConverterFactory
.
import retrofit2.converter.scalars.ScalarsConverterFactory
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
- Agrega la URL 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()
- 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 {
}
- Agrega una función llamada
getPhotos()
a la interfazMarsApiService
para obtener la string de respuesta del servicio web.
interface MarsApiService {
fun getPhotos()
}
- 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 esphotos
. 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.
- 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.
- Fuera de la declaración de la interfaz de
MarsApiService
, define un objeto público llamadoMarsApi
para inicializar el servicio de Retrofit. Este es el objeto singleton público al que puede acceder el resto de la app.
object MarsApi {}
- Dentro de la declaración del objeto
MarsApi
, agrega una propiedad de objeto de Retrofit inicializada de forma diferida y con el nombreretrofitService
del tipoMarsApiService
. 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 {}
}
- Inicializa la variable
retrofitService
usando el métodoretrofit.create()
con la interfazMarsApiService
.
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.
- En el archivo
MarsApiService.kt
, haz quegetPhotos()
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 unviewModelScope
.
@GET("photos")
suspend fun getPhotos(): String
- Abre el archivo
ui/screens/MarsViewModel.kt
. Desplázate hacia abajo hasta el métodogetMarsPhotos()
. Borra la línea que establece la respuesta de estado en"Set the Mars API Response here!"
para que el métodogetMarsPhotos()
esté vacío.
private fun getMarsPhotos() {}
- Dentro de
getMarsPhotos()
, inicia la corrutina medianteviewModelScope.launch
.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
private fun getMarsPhotos() {
viewModelScope.launch {}
}
- Dentro de
viewModelScope
, usa el objeto singletonMarsApi
para llamar al métodogetPhotos()
desde la interfazretrofitService
. Guarda la respuesta que se muestra en unval
llamadolistResult
.
import com.example.marsphotos.network.MarsApi
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
- Asigna el resultado que acabamos de recibir del servidor de 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
- 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.
- 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 conectividad 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
.
- Abre
manifests/AndroidManifest.xml
. Agrega esta línea justo antes de la etiqueta<application>
:
<uses-permission android:name="android.permission.INTERNET" />
- 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.
- 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:
- 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 reiníciala desde Android Studio.
- 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.
- En
getMarsPhotos()
, dentro del bloquelaunch
, agrega un bloquetry
alrededor de la llamadaMarsApi
para controlar las excepciones. - Agrega un bloque
catch
después del bloquetry
.
import java.io.IOException
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
} catch (e: IOException) {
}
}
- 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.
- Abre el archivo
ui/MarsViewModel.kt
. Después de las sentencias de importación, agrega la interfaz selladaMarsUiState
. Esta adición hace que los valores del objetoMarsUiState
puedan ser exhaustivos.
sealed interface MarsUiState {
data class Success(val photos: String) : MarsUiState
object Error : MarsUiState
object Loading : MarsUiState
}
- Dentro de la clase
MarsViewModel
, actualiza la definiciónmarsUiState
. Cambia el tipo aMarsUiState
yMarsUiState.Loading
como valor predeterminado. Haz que el método set sea privado para proteger las escrituras enmarsUiState
.
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
private set
- Desplázate hacia abajo hasta el método
getMarsPhotos()
. Actualiza el valor demarsUiState
aMarsUiState.Success
y pasalistResult
.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
- Dentro del bloque
catch
, controla la respuesta de falla. EstableceMarsUiState
enError
.
catch (e: IOException) {
marsUiState = MarsUiState.Error
}
- Puedes quitar la asignación
marsUiState
del bloquetry-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
}
}
}
- En el archivo
screens/HomeScreen.kt
, agrega una expresiónwhen
enmarsUiState
. Si elmarsUiState
esMarsUiState.Success
, llama aResultScreen
y pasamarsUiState.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()
)
}
}
- Dentro del bloque
when
, agrega verificaciones paraMarsUiState.Loading
yMarsUiState.Error
. Haz que la app muestre los elementos componiblesLoadingScreen
,ResultScreen
yErrorScreen
, 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())
}
}
- 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).
- En el archivo
screens/HomeScreen.kt
, debajo del elemento de componibilidadHomeScreen
, agrega la siguiente función de componibilidadLoadingScreen
para mostrar la animación de carga. El recurso de elemento de diseñoloading_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)
)
}
- Debajo del elemento de componibilidad
LoadingScreen
, agrega la siguiente función de componibilidadErrorScreen
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))
}
}
- 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:
- 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
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.
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
- Abre
build.gradle.kts (Module :app)
. - En el bloque
plugins
, agrega el complementokotlinx serialization
.
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
- En la sección de
dependencies
, agrega el siguiente código para incluir la dependenciakotlinx.serialization
. Esta dependencia proporciona serialización JSON para los proyectos de Kotlin.
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
- Busca las líneas del conversor escalar de Retrofit en el bloque
dependencies
y cámbialo para usarkotlinx-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")
- 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 tipoString
, noInteger
.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
.
- Haz clic con el botón derecho en el paquete network y selecciona New > Kotlin File/Class.
- En el diálogo emergente, selecciona Class y, luego, ingresa
MarsPhoto
como el nombre de la clase. Esto crea un archivo nuevo llamadoMarsPhoto.kt
en el paquetenetwork
. - Para hacer que
MarsPhoto
sea una clase de datos, agrega la palabra clavedata
antes de la definición de la clase. - 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()
- Agrega las siguientes propiedades a la definición de la clase
MarsPhoto
.
data class MarsPhoto(
val id: String, val img_src: String
)
- 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")
.
- 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.
- Abre
network/MarsApiService.kt
. - 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. - Borra la importación de
ScalarConverterFactory
. Corregirás el otro error más tarde.
Quita lo siguiente:
import retrofit2.converter.scalars.ScalarsConverterFactory
- En la declaración del objeto
retrofit
, cambia el compilador de Retrofit para usarkotlinx.serialization
en lugar deScalarConverterFactory
.
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.
- Actualiza la interfaz
MarsApiService
para que Retrofit muestre una lista de objetosMarsPhoto
, en lugar de mostrar unaString
.
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
- Realiza cambios similares en el
viewModel
. AbreMarsViewModel.kt
y desplázate hacia abajo hasta el métodogetMarsPhotos()
.
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.
- 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"
)
- 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:
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.
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 comoString
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 JSONvalue
.
11. Más información
Documentación para desarrolladores de Android:
Documentación de Kotlin:
- Excepciones: probar, capturar, finalmente, arrojar, nada
- Corrutinas, documentación oficial
- Contexto de corrutinas y despachadores
- Serialización | Kotlin
Otra opción: