Título del artículo

Manual de Retrofit 2

Retrofit es una biblioteca que sirve para implementar peticiones HTTP en Android.

Como mínimo necesitas Java 7 o Android 2.3. Además, hay que habilitar la compatibilidad con Java 1.8.

En el build.gradle del módulo app, añade la dependencia y el bloque en el que se da compatibilidad a tu código con Java 1.8:

Consulta cuál es la versión más reciente aquí: https://github.com/square/retrofit

Además también utilizaremos la biblioteca Gson para serializar los objetos de nuestras peticiones:

Tienes un documento de especificaciones de servicios web con una serie de peticiones web que realizar desde tu proyecto, como por ejemplo:

Lista de registros

GET

https://dominio.com/obtenerRegistros

Response:

Nuevo registro

POST

https://dominio.com/crearRegistro

Request:Body

Response:

200

Eliminar registro

DELETE

https://dominio.com/borrarRegistro/idRegistro

Query:

Response:

200

Retrofit genera el código necesario para realizar estas peticiones automáticamente. Para ello es necesario:

  1. Crear un objeto Retrofit con un conversor.
  2. Crear una interfaz con las peticiones (solo una para el ejemplo).
  3. Crear los DTO.
  4. Hacer las peticiones.

Retrofit genera automáticamente clases que implementan las peticiones que queremos poder realizar, en base a la información que le demos en una interfaz. Así pues, crearemos una interfaz que contendrá un método por cada tipo de petición que queramos realizar y en dichos métodos indicaremos sus parámetros y el endpoint.

Por cada petición hay que indicar el tipo de petición mediante las anotaciones @GET, @POST, @DELETE o @PUT. A estas anotaciones se les pasa el endpoint (la URL sin la parte común inicial, por ejemplo, de «https://dominio.com/obtenerRegistros» nos quedamos solo con «obtenerRegistros».) Este endpoint se concatenará al baseUrl (el principio de la URL), que más adelante añadiremos al objeto Retrofit.

@Path

Si en la URL hay parámetros, debemos escribirlos entre llaves ({idRegistro}) y el valor se tomará del parámetro de entrada que lleve la anotación @Path con el mismo nombre que aparece en la URL (@Path("idRegistro") idRegistro: Long).

@Body

Si la petición lleva parámetros en el body (peticiones PUT), estos parámetros se anotan con @Body. Este parámetro tiene que tener los mismos atributos que el JSON de la petición. En el ejemplo de petición PUT, «Nuevo registro», se indica que en el body debe ir este JSON:

Por lo que creamos un objeto como este:

Hablaremos de la correspondencia entre el JSON y los DTO en la sección Conversor.

@Header

Sirve añadir una cabecera a la petición in situ. Puede anotarse con ella tanto el método (si el valor es estático) o uno de los parámetros (si el valor es dinámico):

@Url

Al anotar un parámetro con esta anotación, se utilizará este valor como baseUrl y se ignorará el del objeto Retrofit:

El tipo de retorno es un objeto que, igual que hemos visto en la anotación @Body, tenga los mismos campos que el JSON. Para el JSON de la petición «Nuevo registro»:

{ idRegistro -> Long }

Tendremos que crear una clase como esta:

class RespuestaCrearDTO(val idRegistro: Long?)

Al contrario que en Retrofit 1, donde los métodos podían devolver el objeto de respuesta o un objeto Call que lo envolviese, aquí solamente podemos devolver Call. Si ejecutamos la llamada de forma síncrona o asíncrona depende de si llamamos sobre esta al método execute() o enqueue() (en el siguiente apartado).

Instanciamos la API y realizamos una llamada:

Una llamada síncrona se realiza en el mismo hilo en el que te encuentras, así que tendrás que asegurarte de que no estás en el hilo principal o recibirás una NetworkOnMainThreadException.

Para ejecutar la llamada, llama al método execute() sobre el objeto Call que devuelve la llamda al método de la API. Y si esta falla, se lanzará una excepción que debemos capturar:

La llamada asíncrona se realizará automáticamente en un hilo secundario y la respuesta llegará en el hilo principal, así que no debes preocuparte por el hilo en el que te encuentras.

Para ejecutarla, realiza una llamada al método enqueue() sobre el objeto Call que devuelve la llamada al método de la API. Este método recibe un Callback, que será donde recibamos la respuesta o el error, de vuelta en el hilo principal:

El objeto Retrofit es el encargado de generar clases que materializan nuestras peticiones. Se construye mediante la clase Retrofit.Builder.

Pero, la verdad, así vas a conseguir más bien poco. Es necesario que construyamos el objeto Retrofit con todo lo que vamos a necesitar:

Necesitarás indicarle:

  • La URL base a la que se hacen las peticiones (es decir, la primera parte de la URL que dejamos fuera en el apartado Crear interfaz con las peticiones).

  • El conversor de objetos que vas a usar para que mapee la respuesta de las peticiones a objetos DTO.

  • Un cliente OkHttpClient con algunas modificaciones sobre las peticiones.

Los conversores convierten JSON en objetos. Sirve tanto para enviar objetos en una petición como para convertir en objetos la respuesta de la petición. Recuerda la petición «Nuevo registro»:

Para que nuestra petición pueda convertir el objeto RegistroDTO en el JSON que requiere la petición y para que pueda convertir la respuesta en un objeto RespuestaCrearDTO, es necesario que añadamos un objeto conversor al objeto Retrofit.

En Retrofit 2, los conversores se añaden al Retrofit.Builder mediante su método addConverterFactory().

Por defecto, Retrofit 2 tiene tres fábricas que puedes usar:

  • OptionalConverterFactory
  • ScalarConverterFactory
  • GsonConverterFactory

Una instancia de estas se crea mediante una llamada a su método create(). Sin embargo, si quieres utilizar otro conversor, puedes crear una clase que herede de Converter.Factory y sobrescribir los métodos requestBodyConverter() y responseBodyConverter() usando el conversor que quieras. En este manual, lo haremos con Gson.

Dado que en un JSON solo hay tipos primitivos y arrays, los conversores solo convierten JSON a objetos que estén compuestos por tipos primitivos, listas y/u otros objetos compues-tos por lo mismo. (Cuando hablamos de «tipos primitivos», también nos referimos a los Strings y a las clases de envoltura, o a objetos de Kotlin que se corresponden con tipos primitivos de Java).

Por ejemplo, un convertidor puede convertir el siguiente JSON:

En un objeto de esta clase:

Y viceversa. Y también este JSON:

En una List<RegistroDTO> y viceversa.

Para que Gson pueda hacer esto, el nombre de las propiedades de la clase tiene que coincidir con el de los campos del JSON. Como ves, todas las propiedades del objeto RegistroDTO tienen una clave equivalente en el JSON.

Si no fuera posible que una propiedad tuviera el mismo nombre que la clave en el JSON, Gson dispone de una anotación para la propiedad, en la que se indica el nombre que tiene en el JSON: @SerializedName. Por ejemplo:

Lo visto en la sección anterior debería ser todo lo que hicieras en Retrofit en un proyecto con una buena separación de capas. Sin embargo, el escenario ideal no existe casi nunca, y es muy probable que el DTO no coincida con el JSON, por lo que necesitarás procesar el JSON de forma especial. Por ejemplo, imagina que tienes este DTO en lugar del anterior:

tipo es un enum, mientras que en el JSON es un Int, y fechaCreación es una Date, mientras que en el JSON es un String. Tendríamos que añadir instrucciones para que el convertidor supiera cómo convertir este JSON de respuesta en un RegistroDTO. Para ello tenemos que usar un deserializador.

Un deserializador sirve para enseñar al conversor a convertir JSON en determinados tipos de objeto. Se añaden a un GsonBuilder de esta manera:

Para el ejemplo que venimos arrastrando:

{ "id":5L, "nombre":"registro", "tipo":3, "fechaCreacion":"12-02-2020" }

Este sería el deserializador que tenemos que crear:

Cuando Retrofit reciba la respuesta de una petición y compruebe que tiene que convertirla a un objeto RegistroDTO, llamará al método deserialize() con el trozo de JSON pertinente para que tú lo crees por él.

Dentro del método dispondremos de un JsonElement. Este es la cadena JSON convertida en un objeto. La cadena JSON puede representar un objeto (si comienza por llave ‘{‘) o un array de objetos (si comienza por corchete ‘[‘). En el ejemplo, la respuesta es un objeto, por lo que comprobamos que sea un JsonObject para poder acceder a sus atributos.

El JsonObject obtenido contiene una serie de JsonElements, que son pares clave-valor, que es el contenido de la cadena. Para acceder a estos, se usa el método get() pasándole la clave, y la parseamos con as~ al tipo de datos que representan. Por ejemplo, para obtener el nombre, que es un String, se usa get("nombre").asString.

El valor que viene para la clave tipoRegistro es un Int, pero como esta propiedad en el DTO es un enum, tenemos que utilizar el valor para obtener el enum. Por eso le hacemos un when. Y para la fecha, lo que viene es un String, por lo que la convertimos a fecha con un objeto SimpleDateFormat.

Y finalmente, devolvemos el objeto ya creado.

Si uno de los elementos del objeto RegistroDTO hubiera sido un array, habríamos tenido que utilizar el método asJsonArray. Mira este ejemplo:

DTO

JSON

Al deserializador del aparatado anterior incluiríamos esto para poder deserializar las personas:

Hemos obtenido la parte del JSON que corresponde a personas y hemos comprobado si es una instancia de JsonArray, es decir, si comienza por ‘[‘. Si lo es, solo debemos saber que un JsonArray se utiliza como un array normal lleno de JsonObjects. Se puede llamar a su método length() para saber que longitud tiene y se puede acceder a cada posición con corchetes. Cada posición contendrá un JSON que equivale a una PersonaDTO.

Como habrás podido pensar, el objeto PersonaDTO no requiere un tratamiento especial; está compuesto por primitivos y los campos del JSON se corresponden perfectamente con los del objeto. Por eso, podemos convertirlo automáticamente obteniendo una instancia de Gson y llamando a su método toJson(), como en el ejemplo. Recuerda que Gson sabe convertir automáticamente, un JsonArray en una List.

Pero, alternativamente, podríamos haber convertido el JSON en un una List<PersonaDTO> de forma manual:

Y si el objeto PersonaDTO hubiera requerido de algún procesamiento especial, aún podríamos seguir usando el Gson que hemos creado. Crearíamos un deserializador para PersonaDTO y se lo tendríamos que añadir al GsonBuilder:

Al contrario que un deserializador, el serializador sirve para enseñar al conversor cómo convertir un objeto en un JSON. ¿Cuándo necesitaremos crear un serializador? Cuando tengamos que hacer una petición enviando un JSON y el objeto DTO que le pasemos a la petición difiera de las especificaciones del WS.

Por ejemplo, en una petición POST, casi con total seguridad, tendrás que añadir algún objeto al body de la petición. El objeto DTO que utilicemos debe coincidir con el JSON de la petición (sus campos deben tener el mismo nombre que las claves del JSON). Sin embargo, es posible que nuestro objeto no coincida con la definición del JSON y tengamos que especificar cómo convertirlo en JSON de alguna forma en particular.

Un serializador se añade a un GsonBuilder de la misma manera que un deserializador:

Tomaremos el mismo ejemplo que antes, el complicado, con el array:

Este es el JSON que hay que enviar en la petición «Crear registro», el cual especificamos como un DTO:

Pero este es nuestro objeto:

Como ves, igual que antes, habría que enviar un tipo numérico, pero tenemos un enum, y una fechaCreacion de tipo texto, pero tenemos una Date. Así que crearíamos el siguiente serializador:

Como vemos, el método serialize() recibe un RegistroDTO y tenemos que devolverlo en forma de JsonElement. Básicamente es leer lo que hicimos al implementar el deserializador e invertirlo. Como el JSON que requiere el WS es un objeto (porque empieza por ‘{‘), creamos un JsonObject y le añadimos tipos primitivos con addProperty() y otros JsonElement (como un JsonArray) con add().

Igual que antes, como el objeto PersonaDTO no requiere un tratamiento especial, podemos crear una instancia de Gson y usarla para serializar la persona, como hemos hecho en el ejem-plo, pero también podríamos haberla creado manualmente:

Si para serializar PersonaDTO hubieras necesitado un serializador, recuerda que debes añadirlo a la instancia de Gson que utilices:

Gson tiene un sencillo método que sirve para serializar y deserializar fechas automáticamente: el método GsonBuilder.setDateFormat():

El método puede recibir:

  • Un patrón de fechas que sigue las mismas convenciones que los patrones de SimpleDateFormat.

  • Un estilo de fecha de entre los especificados en la clase DateFormat: SHORT, MEDIUM, LONG, FULL. Sin embargo, se desaconseja utilizarla para los conversores, ya que el patrón cambia según el país, y no se especifica ni en la propia documentación.

¡Cuidado! Si asignas este convertidor a un objeto Retrofit, este sabrá cómo convertir los Strings de las respuestas en un Date y los Dates a Strings en las peticiones, pero estás asumiendo que todas las peticiones van a utilizar el mismo formato de fecha. En caso contrario, seguirás teniendo que hacer un serializador/deserializador si tu DTO tiene Dates.

Por supuesto, tener un deserializador no es incompatible con usar este método, ya que un serializador/deserializador es una conversión manual del objeto, con lo que este método no entrará en juego.

Por defecto, Retrofit ya se construye con un cliente, pero lo más probable es que tú prefieras crear uno propio para poder hacerle ciertas modificaciones sobre las peticiones, como como establecer un time out o añadir cabeceras.

Un cliente se crea de esta manera:

Y se añade al Retrofit.Builder mediante el método client():

Podemos añadir un límite de tiempo para considerar que una petición ha fallado por durar demasiado:

A un cliente se le pueden añadir interceptores de petición. Estos sirven para ejecutar un código antes y después de realizar cualquier petición que se haga mediante dicho cliente.

Los interceptores se añaden al objeto OkHttpClient.Builder mediante el método addInterceptor():

Un interceptor debe implementar la interfaz Interceptor e implementar el método intercept(). Este sería un ejemplo de interceptor que no hace nada:

El método intercept() recibe una Chain, que es una representación del flujo de la petición. Al entrar en el método hemos «interceptado» la petición, por lo que ahora mismo, la llamada está retenida hasta que la reanudemos. Podemos obtenerla mediante el método chain.request() y usarla para hacer cosas con ella.

Tras modificarla, se debe proseguir el flujo llamando al método chain.proceed() pasándole la petición. Esta llamada devuelve la respuesta, y puede fallar si falla la petición (la conversión de la respuesta a objeto), por eso debe ir dentro de un try-catch.

Ahora tienes la respuesta retenida y puedes hacer algo con ella antes de devolverla al final del método. Una petición pasa por todos los interceptores que se añaden a un cliente y no llega a la aplicación hasta después de eso.

El objeto Request te permite realizar las siguientes acciones:

  • Obtener el body de la petición:

    val body: RequestBody = peticion.body()

  • Obtener todas las cabeceras de la petición:

    val cabeceras: List<String> = peticion.headers()

  • Obtener una cabecera:

    val cabecera: String = peticion.header("Content-type")

  • Obtener el método de la petición

  • Saber si la petición es https

    val esSegura = peticion.isHttps()

  • Obtener información sobre la URL:

    val url: HttpUrl = peticion.url()

  • Obtener el RequestBuilder para modificar la petición:

    val requestBuilder = chain.request().newBuilder()

Uno de los usos más comunes de un interceptor es el de añadir cabeceras a las peticiones, y esto puede realizarse mediante un Request.Builder.

En un interceptor podemos capturar y modificar todas las peticiones que este intercepte. Pero las modificaciones no se realizan sobre las peticiones directamente, sino que debemos obtener un Request.Builder a partir de la petición, hacer sobre este las modificaciones que queramos y crea una nueva. Para obtener el Builder, ejecuta lo siguiente en tu interceptor:

El método newBuilder() devuelve un Request.Builder con todos los datos de la petición actual, y sobre este podemos hacer las modificaciones que necesitemos antes de volver a crear la petición con build() y continuar con el flujo.

Entre otras, podemos hacer las siguientes modificaciones:

  • Añade una cabecera a la petición. Las cabeceras son pares clave-valor.

    requestBuilder.addHeader("Accept-language", "en_EN")

  • Eliminar una cabecera indicándole su clave.

    requestBuilder.removeHeader("Accept-language")

  • Modificar el valor de una cabecera ya existente o añadirla si no existía.

    requestBuilder.header("Accept-language", "es_ES")

  • Cambiar el tipo de método de la petición.

    A los métodos put(), delete() y post() puedes añadir un body, (aunque recuerda que ya tiene que el que tenga la petición) o puedes pasarle null para que no tenga.

Puedes consultar la información de la petición mediante un objeto HttpUrl de la petición. Esta se obtiene en el interceptor a partir del objeto Request:

Con ella puedes consultar la siguiente información (suponiendo que la URL es «www.dominio.es/algo/más/todavía?dato=valor&otroDato=otroValor»):

  • Una lista de todas las partes del path (sin incluir la baseUrl) separadas por barras:

  • La query de la URL:

  • Los parámetros de la query:

  • Los nombres de los parámetros de la query:

  • Obtener un builder para modificar la URL:

    val urlBuilder: HttpUrl.Builder = url.newBuilder()

Puedes modificar todo lo relacionado a la URL de la petición mediante un objeto HttpUrl.Builder, el cual se obtiene del objeto HttpUrl en tu interceptor. Este objeto, igual que el Request.Builder, contiene toda la información de la URL actual:

Entre otras cosas, puedes hacer lo siguiente (supongamos que la URL es «www.dominio.com/algo?algunDato=algunValor»):

  • Modificar la query completa de la URL:

  • Añadir un parámetro a la query:

  • Modificar un parámetro de la query:

  • Eliminar todos los valores de la query:

  • Añadir un nuevo segmento al path de la URL:

  • Añadir varios segmentos al path de la URL:

  • Modificar uno de los segmentos del path de la URL:

  • Eliminar un segmento del path de la URL:

Podemos hacer que todas las peticiones y sus respuestas aparezcan por consola añadiendo un interceptor que se encargue de ello. No es necesario que lo hagamos nosotros. Ya existe uno creado (HttpLoggingInterceptor) y solo debemos añadírselo al cliente que añadamos al builder de Retrofit.

Primero añade esta dependencia al build.gradle del directorio app:

Y luego crea una instancia de HttpLoggingInterceptor y añádela al builder del objeto Retrofit:

El log que saldrá por la pestaña 6:Logcat tendrá la etiqueta «D/OkHttp» y el siguiente formato:

0 Comentarios

Contesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

©2024 Codictados Comunidad libre para el aprendizaje de codigo Online

o

Inicia Sesión con tu Usuario y Contraseña

o    

¿Olvidó sus datos?

o

Create Account