Skip to content

10 Retrofit

Retrofit

Retrofit es una librería desarrollada por la empresa Square para crear clientes REST, proporcionándonos un poderoso framework para interactuar con este tipo de web APIs, haciendo uso interno de la librería OkHttp para la gestión de peticiones HTTP. Gracias a ella, se hace más sencillo el proceso de descarga de datos en formato JSON o XML, que posteriormente serán procesados para obtener los objetos POJO (Plain Old Java Object) contenidos en la respuesta. Lo más habitual es que el proceso de procesamiento de la respuesta se lleva a cabo a través de alguna de las librerías para el procesamiento del formato JSON, como Gson, Jackson o Moshi.

La principal ventaja de Retrofit es que permite al desarrollador especificar de forma declarativa los tipos de peticiones (endpoints) a través de una interfaz que contiene un método por tipo de petición, de manera que la declaración del método incluirá una serie de anotaciones específicas de Retrofit, que permiten a la librería determinar la petición real que debe llevarse a cabo. Por ejemplo:

// Interfaz de comunicación con la API (en este caso GitHub).
public interface GitHubService {
    // Petición GET a dicho path, que incluye el usuario recibido.
    // Retorna una lista de objeto Repo.
    @GET("/users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);
}

Para poder utilizar Retrofit será necesario incluir en el archivo build.gradle las siguientes dependencias (Gson sólo la incluiremos si va a ser usada o en lel fichero pom.xml):

dependencies {
    // ...
    implementation 'com.squareup.retrofit2:retrofit:2.X.X'
    implementation 'com.google.code.gson:gson:2.X.X'
    implementation 'com.squareup.retrofit2:converter-gson:2.X.X'
}
<dependencies>
    <dependency>
        <groupId>com.squareup.retrofit2</groupId>
        <artifactId>retrofit</artifactId>
        <version>2.X.X</version>
    </dependency>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.X.X</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.retrofit2</groupId>
        <artifactId>converter-gson</artifactId>
        <version>2.X.X</version>
    </dependency>
</dependencies>

El código de nuestro proyecto deberá incorporar los siguientes elementos:

  • Una clase POJO (modelo) por cada objeto de datos incluido en cualquiera de las respuestas (en el ejemplo anterior, al menos la clase Repo). Para poder obtener directamente las clases a partir de la cadena JSON correspondiente podemos usar las web http://www.jsonschema2pojo.org/. Si queremos que el lenguaje de destino sea Kotlin, podemos usar la web https://www.json2kotlin.com/ o el plugin de IntelliJ https://plugins.jetbrains.com/plugin/9960-json-to-kotlin-class-jsontokotlinclass-/.
  • Una interfaz que incluya la declaración de los métodos correspondientes a los distintos tipos de peticiones (endpoints), con las anotaciones Retrofit adecuadas.
  • Un objeto de la clase Retrofit para la obtención de un cliente REST de acceso a la API. Para obtener el objeto Retrofit usaremos un objeto constructor de la clase Retrofit.Builder, en el que configuraremos distintos aspectos de acceso a la API, como la url base para las peticiones (a través del método baseUrl(), se recomienda que la url indicada siempre termine en /), o el conversor a utilizar para la respuesta (a través del método addConverterFactory()), para finalmente llamar a su método build(), que nos retornará el objeto Retrofit ya configurado.
  • Un objeto creado a través del objeto Retrofit, que implementa la interfaz definida anteriormente, y que actúa como cliente REST, permitiéndonos llevar a cabo las peticiones a la API a través de los métodos de la interfaz.

En el siguiente ejemplo vemos cómo crear el objeto Retrofit a través del configuración y la posterior obtención del cliente REST con el que realizar las peticiones:

// Creamos el objeto Retrofit, indicando que la url base y que se
// use como conversor de la respuesta la librería Gson.
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
// Se crea el cliente REST de acceso a la API a partir del
// objeto Retrofit. 
service = retrofit.create(GitHubService.class);

Una vez obtenido el cliente REST, podemos realizar las peticiones a través de sus métodos, que corresponden a los de la interfaz. Por ejemplo:

// Petición de los repos del usuario pedrojoya.
// La respuesta será procesada para obtener
// una lista de objetos Repo.
Call<List<Repo>> repos = service.listRepos("pedrojoya");

Anotaciones para la declaración de tipos de peticiones

A continuación se muestran ejemplos de declaración de peticiones en las que se usan distintas anotaciones. Se usan comentarios para explicar su funcionamiento:

// Petición GET indicando explícitamente la URL.
@GET
Call<User> getUserByUrl(@Url String url);

// Petición GET simple.
@GET("users/list")
Call<List<User>> listUsers();

// Petición GET con parámetros en la URL.
@GET("users/list?sort=desc")
Call<List<User>> listUsersDescOrder();

// Petición GET. La URL incluye el valor del parámetro groupId.
// Se añade un parámetro a la URL cuyo nombre será sort y
// cuyo valor será el del parámetro sort del método.
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

// Petición GET. La URL incluye el valor del parámetro groupId.
// Se añaden como parámetros de la URL los elementos del mapa (pares clave-valor)
// correspondiente al valor del parámetro options del método.
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);

// Petición POST. Se añade al cuerpo de la petición la representación JSON 
// del objeto usuario correspondiente al valor del parámetro user del método.
@POST("users/new")
Call<User> createUser(@Body User user);

// Petición POST. Se añaden como campos de la URL con los nombres
// first_name y last_name los valores de los parámetros first y last del método
// respectivamente.
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

// Petición GET. Se añade a la cabecera de la petición
// la línea de cabecera especificada.
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

// Petición GET. Se añaden a la cabecera de la petición
// las líneas de cabecera especificadas en formado JSON.
// La URL contendrá el valor del parámetro username del método.
@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

// Petición GET. Se añade a la cabecera de la petición
// una línea de cabecera con la clave Authorization y cuyo valor
// corresponderá al valor del parámetro authorization del método. 
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization);

Peticiones síncronas y asíncronas

A la hora de realizar una petición podemos indicar que se realice síncronamente o asíncronamente:

Cuando realizamos una petición síncrona la aplicación se queda a la espera de la respuesta. Emplearemos el método execute() del objeto Call. Debemos tener en cuenta que en Android NO se puede llamar a este método desde el hilo de la interfaz de usuario porque produciría la excepción NetworkOnMainThreadException, por lo que deberemos llamarlo desde un hilo secundario. Por ejemplo:

Call<Repo> call = service.loadRepo("studio");
// Llamada síncrona. No hacerla en el hilo de la IU.
Response<Repo> response = call.execute();
// Se obtiene el objeto Repo desde response.body()
if (response.body() != null && response.isSuccessful()) {
    Repo repo = response.body();
    // ...
}

Cuando realizamos una petición asíncrona la aplicación sigue ejecutándose, ya que se realiza automáticamente en un hilo secundario. Emplearemos el método enqueue(), al que le pasamos un objeto listener, de la clase Callback<ClaseResultado>, que será notificado cuando se haya obtenido y procesado la respuesta, a través de su método onResponse(), o de su método onFailure() si se ha producido un error. El método onResponse() recibirá como parámetro un objeto Response<ClaseResultado> correspondiente a la respuesta obtenida. Para obtener el objeto resultado a partir de la respuesta, usaremos su método body(). Debemos tener en cuenta que el metodo onResponse() será ejecutado incluso aunque haya habido algún problema con la respuesta, en cuyo caso el método body() de la respuesta retornará null (deberemos controlar esta situación). Por ejemplo:

Call<Repo> call = service.loadRepo("studio");
// Llamada asíncrona (se encola, cuando esté disponible se ejecutará el listener).
call.enqueue(new Callback<Repo>() {
    @Override
    public void onResponse(Call<Repo> call, Response<Repo> response) {
        // Se obtiene el objeto Repo desde response.body()
        if (response.body() != null && response.isSuccessful()) {
            Repo repo = response.body();
            ...
        }
    }

    @Override
    public void onFailure(Call<Repo> call, Throwable t) {
        // Se ha producido un error que ha imposibilitado realizar la petición,
        // por ejemplo que no hay conexión a Internet.
        ...
    }
});

Interceptores de peticiones

En algunas APIs es necesario indicar unas determinadas cabeceras en todas las peticiones que le hagamos. Si este es el caso, en vez de tener que especificar dichas cabeceras en todas las declaraciones de tipos de peticiones, podemos crear nuestro propio objeto de conexión HTTP en el que definamos un interceptor de peticiones.

Retrofit usa internamente OkHttp como librería de comunicación HTTP. No es necesario definir ninguna dependencia al respecto en el archivo build.gradle porque Retrofit ya lo hace internamente, pero nosotros podemos agregar la dependencia explícitamente en el archivo build.gradle o en el pom.xml:

dependencies {
    // ...
    implementation 'com.squareup.okhttp3:okhttp:X.X.X'
}
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>X.X.X</version>
</dependency>

Si no especificamos nada al respecto en el objeto Retrofit.Builder, Retrofit usará un objeto estándar de OkHttp. Sin embargo, nosotros podemos crear nuestro propio objeto OkHttp, configurarlo a nuestro gusto e indicarle a Retrofit.Builder, mediante el método client(), que lo use como cliente que conexión HTTP a la hora de crear nuestro servicio.

Uno de los elementos que podemos personalizar en nuestro objeto OkHttp es la adición de un determinado interceptor de peticiones, cuya misión en este caso será interceptar cualquier petición o respuesta que se realice a través del cliente OkHttp, para reconfigurarla. En el siguiente ejemplo se añade un interceptor al cliente OkHttp para añadir a todas las peticiones que se realicen a través de él una cabecera indicando el User-Agent y un parámetro en la query correspondiente a la api key:

// Se crea el builder para el cliente OkHttp.
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// Se le añade un interceptador al builder.
builder.addInterceptor(new Interceptor() {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        // Se obtiene la petición original.
        Request request = chain.request();
        // Se obtiene una nueva URL basada en la original de la petición
        // agregando el parámetro de consulta correspondiente a la api key.
        HttpUrl newUrl = request.url().newBuilder().addQueryParameter(
                PARAM_API_KEY, apiKey).build();
        // Se crea una nueva petición basándose en la original.
        // Se cambia la url por la nueva modificada y se agrega una cabecera.
        Request newRequest;
        newRequest = request.newBuilder()
                .url(newUrl)
                .addHeader("User-Agent", 
                        "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)")
                .build();
        // Se reemplaza la nueva petición por la antigua y se continúa.
        return chain.proceed(newRequest);
    }
});
// Se construye el cliente OkHttpClient a partir del builder.
OkHttpClient client = builder.build();

Otro interceptor que suele agregarse habitualmente es uno que nos permite mostrar un log de todas las peticiones realizadas a través de Retrofit, así como el contenido de las respuestas. Para ello deberemos agregar la dependencia correspondiente al archivo build.gradle o pom.xml:

dependencies {
    // ...
    implementation 'com.squareup.okhttp3:logging-interceptor:X.X.X'
}
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>logging-interceptor</artifactId>
    <version>X.X.X</version>
</dependency>

Al igual que en el caso de interceptor explicado con anterioridad, tendremos que crear un objeto interceptor HttpLoggingInterceptor, configurar el nivel de log mediante su método setLevel(HttpLoggingInterceptor.Level.BODY) y añadirlo al cliente OkHttp. Existen niveles que muestran menos información, como por ejemplo HttpLoggingInterceptor.Level.BASIC.

Así, el proceso de construcción completo sería:

Construcción de un objeto Retrofit y cliente REST con logging

// Se crea el interceptor
HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor();
logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// Se crea el cliente OkHttp
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(logInterceptor)
    .build();
// Se crea el objeto Retrofit
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();
// Se crea el cliente REST de acceso a la API a partir del
// objeto Retrofit. 
GitHubService apiService = retrofit.create(GitHubService.class);

Procesador de respuestas

Otro de los elementos que deberemos indicar a la hora de construir el objeto Retrofit es qué procesador (converter) debe utilizarse para procesar la respuesta, para lo que empleamos el método addConverterFactory().

Aunque es posible crear nuestra propia clase procesadora que implemente la interfaz Converter.Factory, lo más habitual será usar alguno de los procesadores soportados oficialmente por Retrofit, correspondientes a conocidas librerías de procesamiento JSON o XML, en cuyo caso deberemos añadir la dependencia correspondiente a nuestro proyecto.

Conversores de JSON:

  • Gson: implementation "com.squareup.retrofit2:converter-gson:X.X.X"
  • Jackson: implementation 'com.squareup.retrofit2:converter-jackson:X.X.X'
  • Moshi: implementation 'com.squareup.retrofit2:converter-moshi:X.X.X'

Conversores de XML:

  • Simple XML: implementation 'com.squareup.retrofit2:converter-simplexml:X.X.X'

Conversores primitivos:

  • Scalars (tipos primitivos, clases de tipos primitivos y String): implementation 'com.squareup.retrofit2:converter-scalars:X.X.X'.

Por ejemplo, si queremos que Retrofit use Gson para procesar las respuestas, deberemos añadir las siguientes dependencia en el archivo build.gradle:

dependencies {
    // ...
    implementation 'com.squareup.retrofit2:retrofit:X.X.X'
    implementation 'com.google.code.gson:gson:Y.Y.Y'
    implementation 'com.squareup.retrofit2:converter-gson:X.X.X'
}

y a la hora de crear el objeto Retrofit tendremos que indicar que se use dicho procesador:

Configuración de Retrofit para uso de Gson

// Creamos el objeto Retrofit, indicando que se
// use como conversor de la respuesta la librería Gson.
Retrofit retrofit = new Retrofit.Builder()
        // ...
        .addConverterFactory(GsonConverterFactory.create())
        .build();

Proyecto Retrofit

En este proyecto vamos a realizar una aplicación que realiza distintos tipos de peticiones HTTP a un servicio REST de ejemplo, usando la librería Retrofit. La respuesta es pareada y mostradas por consola.

Definimos de la interfaz ApiService, en la que informamos a Retrofit de cómo queremos atacar la Web API REST. Nuestro main construye el objeto ApiService. Para ello antes construye un objeto Retrofit con la configuración adecuada, que incluye un objeto OkHttpClient personalizado para que muestre un log en consola con las peticiones y las respuestas obtenidas. En el main también realizamos las distintas peticiones.

import es.iessaladillo.pedrojoya.retrofit.data.api.ApiService;
import es.iessaladillo.pedrojoya.retrofit.data.api.response.PostResponse;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.jetbrains.annotations.NotNull;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import java.io.IOException;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public class Main {

    private final ApiService apiService = createApiService();
    private final Scanner scanner = new Scanner(System.in);

    Main() {
        int selectedOption = showMenu();
        processOption(selectedOption);
    }

    public static void main(String[] args) {
        new Main();
    }

    private int showMenu() {
        System.out.println("\nMENU");
        System.out.println("1. Show all posts synchronously");
        System.out.println("2. Show all posts with callback");
        System.out.println("3. Show all posts");
        System.out.println("4. Show post");
        System.out.println("5. Show user's posts");
        System.out.println("6. Show posts by user");
        System.out.println("7. Create post");
        System.out.println("8. Update post");
        System.out.println("9. Update post partially");
        System.out.println("10. Delete post");
        System.out.println("11. Show only headers");
        System.out.println("12. Show access options");
        System.out.print("Select an option: ");
        try {
            return scanner.nextInt();
        } catch (Exception e) {
            scanner.nextLine();
            return 0;
        }
    }

    private void processOption(int selectedOption) {
        switch (selectedOption) {
            case 1:
                showPostsSync();
                break;
            case 2:
                showPostsWithCallback();
                break;
            case 3:
                showPosts();
                break;
            case 4:
                showPost(1);
                break;
            case 5:
                showUserPosts(1);
                break;
            case 6:
                showPostsByUser(1);
                break;
            case 7:
                createPost(new PostResponse(1, 0, "Baldomero", "Llégate Ligero"));
                break;
            case 8:
                updatePost(1, new PostResponse(1, 1, "Baldomero", "Llégate Ligero"));
                break;
            case 9:
                patchPost(1, new PostResponse(3, 1, "Baldomero", "Llégate Ligero"));
                break;
            case 10:
                deletePost(1);
                break;
            case 11:
                showPostsHeaders();
                break;
            case 12:
                showPostsAccessOptions();
                break;
        }
    }

    private void showPostsSync() {
        try {
            Response<List<PostResponse>> response = apiService.getPostsCall().execute();
            if (response.isSuccessful() && response.body() != null) {
                showModel(response.body());
            }
        } catch (IOException e) {
            showError(e);
        }
    }

    private void showPostsWithCallback() {
        apiService.getPostsCall().enqueue(new Callback<List<PostResponse>>() {
            @Override
            public void onResponse(Call<List<PostResponse>> call, Response<List<PostResponse>> response) {
                if (response.isSuccessful() && response.body() != null) {
                    showModel(response.body());
                }
            }

            @Override
            public void onFailure(Call<List<PostResponse>> call, Throwable t) {
                System.out.println(t.toString());
            }
        });
    }

    private void showPosts() {
        apiService.getPosts().whenComplete(this::showErrorOrModel);
    }

    private void showPostsAccessOptions() {
        apiService.getPostsAccessOptions()
                .thenApply(response ->
                        String.join(";", response.headers().values("access-control-allow-methods")))
                .whenComplete(this::showErrorOrModel);
    }

    private void showPostsHeaders() {
        apiService.getPostsHeaders()
                .thenApply(response -> response.headers().toMultimap().entrySet().stream()
                        .map(entry -> entry.getKey() + ": " + String.join(";", entry.getValue()))
                        .collect(Collectors.joining("\n"))
                )
                .whenComplete(this::showErrorOrModel);
    }

    private void showPost(int postId) {
        apiService.getPost(1).whenComplete(this::showErrorOrModel);
    }

    private void createPost(PostResponse postResponse) {
        apiService.createPost(postResponse).whenComplete(this::showErrorOrModel);
    }

    private void updatePost(int postId, PostResponse postResponse) {
        apiService.updatePost(postId, postResponse).whenComplete(this::showErrorOrModel);
    }

    private void patchPost(int postId, PostResponse postResponse) {
        apiService.patchPost(postId, postResponse).whenComplete(this::showErrorOrModel);
    }

    private void deletePost(int postId) {
        apiService.deletePost(postId).whenComplete(this::showErrorOrModel).join();
    }

    private void showUserPosts(int userId) {
        apiService.getUserPosts(userId).whenComplete(this::showErrorOrModel).join();
    }

    private void showPostsByUser(int userId) {
        apiService.getPostsByUser(userId).whenComplete(this::showErrorOrModel).join();
    }

    // -------------------------

    @NotNull
    private ApiService createApiService() {
        HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor();
        logInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
        OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(logInterceptor).build();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://jsonplaceholder.typicode.com/")
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        return retrofit.create(ApiService.class);
    }

    @NotNull
    private ApiService createSwapiService() {
        HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor();
        logInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
        OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(logInterceptor).build();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://swapi.co/api/")
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        return retrofit.create(ApiService.class);
    }

    private <T> void showErrorOrModel(T model, Throwable throwable) {
        if (throwable != null) {
            showError(throwable);
        } else {
            showModel(model);
        }
    }

    private void showError(Throwable e) {
        System.out.println(e.toString());
    }

    private <T> void showModel(T model) {
        System.out.println(model);
    }

}
import es.iessaladillo.pedrojoya.retrofit.data.api.response.PostResponse;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.*;

import java.util.List;
import java.util.concurrent.CompletableFuture;

public interface ApiService {

    @GET("posts")
    Call<List<PostResponse>> getPostsCall();

    @GET("posts")
    CompletableFuture<List<PostResponse>> getPosts();

    @OPTIONS("posts")
    CompletableFuture<Response<PostResponse>> getPostsAccessOptions();

    @HEAD("posts")
    CompletableFuture<Response<Void>> getPostsHeaders();

    @GET("posts/{postId}")
    public CompletableFuture<PostResponse> getPost(@Path("postId") int postId);

    @POST("posts")
    CompletableFuture<PostResponse> createPost(@Body PostResponse postResponse);

    @PUT("posts/{postId}")
    CompletableFuture<PostResponse> updatePost(@Path("postId") int postId, 
                                            @Body PostResponse postResponse);

    @PATCH("posts/{postId}")
    public CompletableFuture<PostResponse> patchPost(@Path("postId") int postId, 
                                                    @Body PostResponse postResponse);

    @DELETE("posts/{postId}")
    CompletableFuture<Void> deletePost(@Path("postId") int postId);

    @GET("users/{userId}/posts")
    CompletableFuture<List<PostResponse>> getUserPosts(@Path("userId") int userId);

    @GET("posts")
    CompletableFuture<List<PostResponse>> getPostsByUser(@Query("userId") int userId);

}
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class PostResponse {

    @SerializedName("userId")
    @Expose
    private Integer userId;
    @SerializedName("id")
    @Expose
    private Integer id;
    @SerializedName("title")
    @Expose
    private String title;
    @SerializedName("body")
    @Expose
    private String body;

    public PostResponse(Integer userId, Integer id, String title, String body) {
        this.userId = userId;
        this.id = id;
        this.title = title;
        this.body = body;
    }

    @Override
    public String toString() {
        return "{" +
                "userId=" + userId +
                ", id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}