Manual de Retrofit 2
Retrofit es una biblioteca que sirve para implementar peticiones HTTP en Android.
1. Añadir dependencias |
↥ | ⇊ | ↧ | ⇶ |
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:
1 2 3 4 5 6 7 8 9 10 |
android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' } |
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:
1 2 |
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' |
2. Guía rápida |
↥ | ⇈ | ⇊ | ⇶ |
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:
- Crear un objeto Retrofit con un conversor.
123456val gson: Gson = GsonBuilder().create()val gsonFactory = GsonConverterFactory.create(gson)val retrofit: Retrofit = Retrofit.Builder().baseUrl("https://dominio.com").addConverterFactory(gsonFactory).build() - Crear una interfaz con las peticiones (solo una para el ejemplo).
12@POST("crearRegistro")fun crearRegistro(@Body registro: RegistroDTO): Call<RespuestaCrearDTO>
- Crear los DTO.
123456class RegistroDTO(val id: Long?,val nombre: String?val tipo: Int?,val fechaCreacion: String?)class RespuestaCrearDTO(val idRegistro: Long?)
- Hacer las peticiones.
1234567891011121314val api: RegistroWs = retrofit.create(RegistrosWs::class)val peticion: Call<RespuestaCrearDTO> = api.crearRegistro()val respuesta: RespuestaCrearDTO = peticion.enqueue(object: Callback<RespuestaCrearDTO> {override fun onResponse(call: Call<RespuestaCrearDTO>,response: Response<RespuestaCrearDTO>) {val respuesta: RespuestaCrearDTO = response.body()// Aquí ya tenemos el resultado}override fun onFailure(call: Call<RespuestaCrearDTO>, t: Throwable) {// Aquí debemos tratar el error y hacer algo en la UI})
3. Crear interfaz con las peticiones y DTO |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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.
1 2 3 4 5 6 7 8 9 10 11 12 |
interface RegistrosWs { @GET("obtenerRegistros") fun obtenerRegistros(): Call<List<RegistroDTO>> @POST("crearRegistro") fun crearRegistro(@Body registro: RegistroDTO): Call<RespuestaCrearDTO> @DELETE("borrarRegistro/{idRegistro}") fun borrarRegistro(@Path("idRegistro") idRegistro: Long): Call<RespuestaBorrarDTO> } |
3.1. Anotaciones |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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.
3.1.1. Parámetros de entrada |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
@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:
1 2 3 4 5 6 |
{ id -> null nombre -> Texto tipo -> Numérico fechaCreacion -> Texto (formato DD-MM-YYYY) } |
Por lo que creamos un objeto como este:
1 2 3 4 |
class RegistroDTO(val id: Long?, val nombre: String?, val tipo: Int?, val fechaCreacion: String?) |
Hablaremos de la correspondencia entre el JSON y los DTO en la sección Conversor.
3.1.2. Cabeceras |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
@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):
1 2 3 4 5 6 |
@Header("Content-type: json") @GET("obtenerRegistros") fun obtenerRegistros(): Call<List<RegistroDTO>> @GET("obtenerRegistros") fun obtenerRegistros(@Header("Content-type") type: String): Call<List<RegistroDTO>> |
Otras anotaciones |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
@Url
Al anotar un parámetro con esta anotación, se utilizará este valor como baseUrl y se ignorará el del objeto Retrofit:
1 2 |
@GET fun obtenerRegistros(@Url miUrl: String): Call<List<RegistroDTO>> |
3.2. Tipo de retorno |
↥ | ⇈ | ⇊ | ⇶ |
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).
1 2 3 4 5 |
@DELETE("borrarRegistro/{idRegistro}") fun borrarRegistro(@Path("idRegistro") idRegistro: Long): Call<RespuestaBorrarDTO> @GET("obtenerRegistros") fun obtenerRegistros(): Call<List<RegistroDTO>> |
4. Hacer la llamada |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
Instanciamos la API y realizamos una llamada:
1 |
val api: RegistroWs = retrofit.create(RegistrosWs::class.java) |
4.1. Llamada síncrona |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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:
1 2 3 4 5 6 |
val respuesta: RespuestaBorrarDTO? = try { api.borrarRegistro(57L).execute() } catch (t: Throwable) { // Aquí habrá que hacer algo con el error null } |
4.2. Llamada asíncrona |
↥ | ⇈ | ⇊ | ⇶ |
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:
1 2 3 4 5 6 7 8 9 10 11 |
api.obtenerRegistros().enqueue(object: Callback<List> { override fun onResponse(call: Call<List>, response: Response<<List>) { val registros: List = response.body() // Aquí ya tenemos el resultado } override fun onFailure(call: Call<<List>, t: Throwable) { // Aquí debemos tratar el error y hacer algo en la UI } ) |
5. Crear el objeto Retrofit |
↥ | ⇈ | ⇊ | ⇶ |
El objeto Retrofit es el encargado de generar clases que materializan nuestras peticiones. Se construye mediante la clase Retrofit.Builder.
1 |
val retrofit: Retrofit = Retrofit.Builder().build() |
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:
1 2 3 4 5 |
val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://dominio.com") .addConverterFactory(gsonFactory) .client(okHttpClient) .build() |
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.
6. Conversor |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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»:
1 2 |
@POST("crearRegistro") fun crearRegistro(@Body registro: RegistroDTO): Call<RespuestaCrearDTO> |
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.
6.1. Converter Factories |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
En Retrofit 2, los conversores se añaden al Retrofit.Builder mediante su método
addConverterFactory()
.
1 2 3 4 5 |
val gson: Gson = GsonBuilder().create() val gsonFactory = GsonConverterFactory.create(gson) val retrofit: Retrofit = Retrofit.Builder() […] .addConverterFactory(gsonFactory) |
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.
6.2. Conversión básica |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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:
1 |
{ "id":5L, "nombre":"registro", "tipo":3, "fechaCreacion":"12-02-2020" } |
En un objeto de esta clase:
1 2 3 4 |
class RegistroDTO(val id: Long? val nombre: String?, val tipo: Int?, val fechaCreacion: String?) |
Y viceversa. Y también este JSON:
1 2 3 4 |
[ { "id":5L, "nombre":"Registro A", "tipo":0, "fechaCreacion":"12-02-2020" }, { "id":6L, "nombre":"Registro B", "tipo":1, "fechaCreacion":"12-02-2017" } ] |
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:
1 2 3 4 |
class RegistroDTO(@SerializedName("id") val identificador: Long? val nombre: String?, val tipo: Int?, @SerializedName("fechaCreacion") val fecha: String?) |
6.3. Conversión personalizada |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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:
1 2 3 4 |
class RegistroDTO(val id: Long? val nombre: String?, val tipo: TipoRegistro?, val fechaCreacion: Date?) |
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.
6.3.1. 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:
1 2 3 |
val gson = GsonBuilder() .registerTypeAdapter(RegistroDTO::class.java, RegistroDTODeserializer()) .create() |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class RegistroDTODeserializer : JsonDeserializer<RegistroDTO> { override fun deserialize(json: JsonElement, typeOfT: Type, context: jsonDeserializationContext) : RegistroDTO? throws JsonParseException = if (json == null || json.isJsonNull || json !is JsonObject) { null } else { val registroJson = json.asJsonObject val id = registroJson.get("id").asLong val nombre = registroJson.get("nombre").asString val tipo = getTipoRetorno(registroJson.get("tipoRegistro").asInt) val fecha = getFecha(registroJson.get("fecha").asString) RegistroDTO(id, nombre, tipo, fecha) } private fun getTipoRetorno(tipo: Int) = when (tipo) { 0 -> TipoRegistro.NUEVO 1 -> TipoRegistro.ANTIGUO else -> null } private fun getFecha(fecha: String) = SimpleDateFormat("dd-MM-yyyy").parse(fecha) } |
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.
6.3.2. Arrays |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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
1 2 3 4 5 |
class RegistroDTO(val id: Long? val nombre: String?, val tipo: TipoRegistro?, val fechaCreacion: Date? val personas: List<PersonaDTO?>>?) |
JSON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "id":5L, "nombre":"registro", "tipo":3, "fechaCreacion":"12-02-2020", "personas": [ { "nombre":"Pedro" "apellido":"Suárez" }, { "nombre":"Julián" "apellido":"González" } ] } |
Al deserializador del aparatado anterior incluiríamos esto para poder deserializar las personas:
1 2 3 4 5 6 7 8 9 10 11 12 |
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): RegistroDTO? throws JsonParseException = val registroJson = json.asJsonObject […] val personas = getPersonas(registroJson.get("personas")) RegistroDTO(id, nombre, tipo, fecha, personas) } private fun getPersonas(json: JsonElement) = json.takeIf { it is JsonArray }?.asJsonArray?.let { GsonBuilder().create().fromJson(it, PersonaDTO::class.java) } |
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:
1 2 3 4 5 6 7 |
json.takeIf { it is JsonArray }?.asJsonArray?.mapNotNull { item -> item?.let { val nombre = it.get("nombre").asString val apellido = it.get("apellido").asString PersonaDTO(nombre, apellido) } } |
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:
1 2 3 4 5 6 7 8 |
private fun getPersonas(json: JsonElement) = json.takeIf { it is JsonArray }?.asJsonArray?.let { val deserializadorPersona = PersonaDTODeserializador() GsonBuilder() .registerTypeAdapter(PersonaDTO::class.java, deserializadorPersona) .create() .fromJson(it, PersonaDTO::class.java) } |
6.3.3. Serializador |
↥ | ⇈ | ⇊ | ↧ | ⇶ |
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:
1 2 3 |
val gson = GsonBuilder() .registerTypeAdapter(RegistroDTO::class.java, RegistroDTODeserializer()) .create() |
Tomaremos el mismo ejemplo que antes, el complicado, con el array:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "id":5L, "nombre":"registro", "tipo":3, "fechaCreacion":"12-02-2020", "personas": [ { "nombre":"Pedro" "apellido":"Suárez" }, { "nombre":"Julián" "apellido":"González" } ] } |
Este es el JSON que hay que enviar en la petición «Crear registro», el cual especificamos como un DTO:
1 2 |
@POST("crearRegistro") fun crearRegistro(@Body registro: RegistroDTO): Call<RespuestaCrearDTO> |
Pero este es nuestro objeto:
1 2 3 4 5 |
class RegistroDTO(val id: Long? val nombre: String?, val tipo: TipoRegistro?, val fechaCreacion: Date? val personas: List<PersonaDTO?>?) |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class RegistroDTOSerializer: JsonSerializer<RegistroDTO> { override fun serialize(registro: RegistroDTO): JsonElement { val json = JsonObject() registro.id?.let { json.addProperty("id", it) } registro.nombre?.let { json.addProperty("nombre", it) } registro.tipo?.let { json.addProperty("tipo", it) } registro.fechaCreacion?.let { json.addProperty("fechaCreacion", formatearFechaCreacion(it)) } registro.personas?.let { json.add("personas", crearArrayPersonas(it)) } } private fun formatearFechaCreacion(fecha: Date) = try { SimpleDateFormat("dd-MM-yyyy").format(fecha) } catch (exception: ParseException) { null } private fun crearArrayPersonas(personas: List<PersonaDTO?>) = JsonArray().apply { val gson = GsonBuilder().create() personas.forEach { persona -> persona?.let { add(gson.toJson(it)) } } } } |
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:
1 2 3 4 |
JsonObject().apply { nombre?.let { addProperty("nombre", it) } apellido?.let { addProperty("apellido", it) } } |
Si para serializar PersonaDTO hubieras necesitado un serializador, recuerda que debes añadirlo a la instancia de Gson que utilices:
1 2 3 4 5 6 7 8 9 10 11 |
private fun crearArrayPersonas(personas: List<PersonaDTO?>) = JsonArray().apply { val deserializadorPersona = PersonaDTODeserializador() val gson = GsonBuilder() .registerTypeAdapter(PersonaDTO::class.java, deserializadorPersona) .create() personas.forEach { persona -> persona?.let { add(gson.toJson(it)) } } } |
6.3.4. Fechas |
↥ | ⇈ | ⇊ | ⇶ |
Gson tiene un sencillo método que sirve para serializar y deserializar fechas automáticamente: el método
GsonBuilder.setDateFormat()
:
1 2 3 4 |
val gson = GsonBuilder(). […] setDateFormat("yyyy-MM-dd HH:mm") .build() |
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.
7. Cliente |
↥ | ⇈ | ↧ | ⇶ |
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:
1 |
val client: OkHttpClient = OkHttpClient.Builder().build() |
Y se añade al Retrofit.Builder mediante el método client()
:
1 2 3 4 |
val retrofit: Retrofit = Retrofit.Builder() […] .client(client) .build() |
7.1. Time outs |
⇈ | ↧ | ⇶ |
Podemos añadir un límite de tiempo para considerar que una petición ha fallado por durar demasiado:
1 2 3 4 5 |
val builder: OkHttpClient.Builder = OkHttpClient.Builder() .connectTimeout(40, TimeUnit.SECONDS) .readTimeout(40, TimeUnit.SECONDS) .writeTimeout(40, TimeUnit.SECONDS) .build(); |
7.2. Interceptores |
↥ | ⇈ | ↧ | ⇶ |
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()
:
1 2 3 4 5 |
val interceptor = MiInterceptor() val builder = OkHttpClient.Builder() […] .addInterceptor(interceptor) .build(); |
Un interceptor debe implementar la interfaz Interceptor e implementar el método
intercept()
. Este sería un ejemplo de interceptor que no hace nada:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class MiInterceptor(): Interceptor { @Throws(IOException::class) override fun intercept(chain: Chain): Response { // Obtener la petición val peticion: Request = chain.request() // Aquí se harían modificaciones // Realizar la petición val respuesta = try { chain.proceed(peticion) } catch (e: Exception) { Log.e("Retrofit", "La petición ha fallado.") throw e; } // Devolver la respuesta a la aplicación return respuesta } } |
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.
7.2.1. Request |
↥ | ⇈ | ↧ | ⇶ |
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
12val metodo = peticion.method()// Puede ser "POST", "PUT", "GET", "DELETE" o "HEAD".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.
7.2.2. 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:
1 2 3 4 5 6 7 8 9 10 11 |
@Throws(IOException::class) override fun intercept(chain: Chain): Response { val requestBuilder = chain.request().newBuilder() // Modificaciones aquí y luego creas una petición val peticionNueva = requestBuilder.build() // Y continuas el flujo con la nueva petición val respuesta = try { chain.proceed(peticionNueva) […] |
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.
12345requestBuilder.get()requestBuilder.put(null)requestBuilder.delete(null)requestBuilder.post(null)requestBuilder.head()A los métodos
put()
,delete()
ypost()
puedes añadir un body, (aunque recuerda que ya tiene que el que tenga la petición) o puedes pasarlenull
para que no tenga.
7.2.2.1. HttpUrl |
↥ | ⇈ | ↧ | ⇶ |
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:
1 2 |
val peticion = chain.request() val url: HttpUrl = peticion.url |
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:12val segments: List<String> = url.pathSegments().// Resultado: ["algo", "más", "todavía"]La query de la URL:
12val query: String = url.query()// Resultado: "dato=valor&otroDato=otroValor"Los parámetros de la query:
123// Se pueden consultar por índice o por nombreval dato = url.queryParamterValues("dato") // Resultado: ["valor"]val otroDato = url.queryParamterValue(1) // Resultado: "valor"Los nombres de los parámetros de la query:
123// Se puede obtener uno o un Set con todosval nombre = url.queryParamterName(0) // Resultado: "dato"val nombres = url.queryParamterNames() // Resultado: ["dato", "otroDato"]Obtener un builder para modificar la URL:
val urlBuilder: HttpUrl.Builder = url.newBuilder()
7.2.2.2. HttpUrl.Request |
↥ | ⇈ | ↧ | ⇶ |
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:
1 2 3 |
val peticion = chain.request() val url: HttpUrl = peticion.url val urlBuilder: HttpUrl.Builder = url.newBuilder() |
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:
12urlBuilder.query("dato=valor&dato2=valor2")// Resultado: "www.dominio.com/algo?dato=valor&dato2=valor2"Añadir un parámetro a la query:
12urlBuilder.addQueryParameter("dato", "valor")// Resultado: "www.dominio.com/algo?algunDato=algunValor&dato=valor"Modificar un parámetro de la query:
123urlBuilder.setQueryParameter("algunDato", "otroValor")// Resultado: "www.dominio.com/algo?algunDato=otroValor"// Si existe, lo remplaza, si no, lo añade.Eliminar todos los valores de la query:
12urlBuilder.removeAllQueryParameters()// Resultado: "www.dominio.com/algo"Añadir un nuevo segmento al path de la URL:
12urlBuilder.addPathSegment("más").// Resultado: "www.dominio.com/algo/más?algunDato=algunValor"Añadir varios segmentos al path de la URL:
12url.Builder.addPathSegments("más/todavía").// Resultado: "www.dominio.com/algo/más/todavía?algunDato=algunValor"Modificar uno de los segmentos del path de la URL:
1234567// Inicial: "www.dominio.com/algo/más/todavía?algunDato=algunValor"urlBuilder.pathSegments().indexOfFirst { it.name == "todavía" }.takeIf { it != -1 }?.let { indice ->urlBuilder.setPathSegment(indice, "aún")}// Resultado: "www.dominio.com/algo/más/aún?algunDato=algunValor"Eliminar un segmento del path de la URL:
123// Inicial: "www.dominio.com/algo/más/todavía?algunDato=algunValor"urlBuilder.removePathSegment(2)// Resultado: "www.dominio.com/algo/más?algunDato=algunValor"
7.3. Habilitar logging de peticiones |
↥ | ⇈ | ⇶ |
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
:
1 |
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' |
Y luego crea una instancia de HttpLoggingInterceptor y añádela al builder del objeto Retrofit:
1 2 3 4 5 6 7 8 9 10 11 12 |
val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val cliente = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) […] .build(); val retrofit = Retrofit.Builder() .client(cliente) […] .build() |
El log que saldrá por la pestaña 6:Logcat tendrá la etiqueta «D/OkHttp
» y el siguiente formato:
1 2 3 4 5 6 7 8 9 10 11 |
D/OkHttp: --> GET http://dominio.com/endpoint_al_que_hayas_llamado D/OkHttp: --> END GET D/OkHttp: <-- 200 OK http://dominio.com/endpoint_al_que_hayas_llamado (263ms) D/OkHttp: x-powered-by: Express D/OkHttp: content-type: text/html; charset=utf-8 D/OkHttp: content-length: 20 D/OkHttp: etag: W/"14-z3iZXchEt5DVWZKsMncy8Wl4KSQ" D/OkHttp: date: Thu, 23 Jul 2020 19:12:09 GMT D/OkHttp: connection: keep-alive D/OkHttp: D/OkHttp: <-- END HTTP (20-byte body) |