Aprenda a realizar requisições na web com RetroFit no Android (Kotlin)

Introdução ao Retrofit

Observação: O repositório que contem o conteúdo desta aula é o Retrofit-Kotlin.

O que é o RetroFit?

Assim como em qualquer linguagem de programação, sempre temos a disposição uma biblioteca que é capaz de realizar requisições na web.

Requisições estas como o envio de dados e o recebimento de dados.

As vezes queremos enviar uma requisição para um servidor da web, afim de recuperar uma listagem de usuários, por exemplo.

Ou quem sabe enviar a este mesmo servidor, alguns dados que o usuário digitou durante a utilização do aplicativo.

Basicamente, o RetroFit é uma camada de abstração muito utilizada para fazer chamadas em APIs.

Sem o RetroFit, precisamos fazer o uso de outras bibliotecas mais antigas do Android (como a Volley por exemplo) que também são capazes de fazer requisições na web.

Já com o RetroFit, por se tratar de uma camada de abstração, ele tende a simplificar muito nossos códigos, tornando-os simples e mais fáceis de serem escritos.

É importante ressaltar que no caso do RetroFit, por de baixo dos panos, ele ainda faz o uso dessas bibliotecas antigas do Android, ok?

A diferença é que o uso dessa biblioteca abstrai a dificuldade, fazendo com que nós desenvolvedores, não precisemos escrever muitos comandos para realizar uma simples requisição na web.

Estrutura do RetroFit

Para a sua implementação, ele conta com 3 camadas diferentes, vejamos:

Entities) Significa "Entidades", e elas mapeiam os modelos de negócio da sua aplicação.

É aqui que nós configuramos os parâmetros que iremos enviar e receber das requisições da web.

Service) Significa "Serviços", e elas mapeiam os EndPoints da nossa aplicação.

Que nada mais são do que as URLs que iremos chamar durante nossas requisições.

Além disso, ela consegue mapear o tipo de retorno (JSON, XML), os verbos utilizados (GET, PUT, POST, DELETE...), headers e entre outros.

RetroFit) É a camada mais importante, que contém a biblioteca em si, e faz a orquestração das chamadas.

Estrutura Inicial do Projeto

Com o Android Studio aberto, crie um novo projeto (File > New > New Project...).

Selecione a opção "Phone and Tablet" e escolha o bloco chamado "Empty Views Activity":

Clique em Next, e na próxima tela, informe o nome do seu aplicativo (eu nomeei como "RetroFit"), e na versão mínima do SDK eu escolhi a versão 7.0.

Por fim, clique em Finish para criar o projeto.

Implementação no Gradle

Como o Retrofit é uma biblioteca do Android, precisamos fazer a sua importação no arquivo build.gradle (Module:app), existente na pasta Gradle Scripts:

Já dentro do bloco dependencies, você vai precisar implementar esses dois recursos:

// RetroFit Dependências
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

Observe que implementamos duas bibliotecas, ambas na versão mais recente 2.9.0 (Agosto de 2023).

Por fim, basta clicar em Sync Now para sincronizar, e clicar em "Build > Make Project" no canto superior do Android Studio para testar se esta tudo certo.

Tudo certo? O projeto compilou sem nenhum erro? Então vamos continuar com nosso aprendizado.

Pedindo Permissão para acessar a Internet

Como o RetroFit é uma biblioteca que acessa a internet, você como desenvolvedor precisa pedir a permissão para fazer isso.

Sendo assim, abra o arquivo chamado AndroidManifests.xml existente na pasta manifest, e adicione o seguinte comando acima da declaração <application...>:

<uses-permission android:name="android.permission.INTERNET" />

Trabalhando com o RetroFit

É importante ressaltar que durante algumas partes da implementação do RetroFit, nós iremos ver diversas anotações, que são as famosas annotations que a biblioteca usa.

Neste conteúdo nós iremos trabalhar com a API gratuita chamada JsonPlaceholder, mais especificamente com o ENDPOINT POSTS, que possui o seguinte retorno do método GET.

[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
]

Consulte o link acima para entender mais a fundo os outros tipos de retornos.

Criando a sua Entidade

Em primeiro lugar, vamos começar configurando a nossa camada de entidades, que é o arquivo que vai armazenar cada um daqueles parâmetros que serão retornados pelo JsonPlaceHolder, que são:

  • userId
  • id
  • title
  • body

Dentro da pasta principal do seu projeto (com.example.retrofit), clique com o botão direito em cima dessa pasta, e crie uma nova Package (New > Package).

No meu caso eu nomeie ela como retrofit:

Dentro dela, voce vai precisar criar um novo arquivo do tipo Kotlin Class/File chamado de PostEntity.kt:

package com.example.retrofit.retrofit

class PostEntity {
}

Perfeito, feito isso, vamos configurar todos os atributos dessa classe de modo que contenham os mesmos nomes dos parâmetros que serão retornados pela API do JsonPlaceHolder:

package com.example.retrofit.retrofit

import com.google.gson.annotations.SerializedName

class PostEntity {

@SerializedName("userId")//Usamos a anotação do gson para mapear o parâmetro que virá pelo Json para dentro do atributo da classe abaixo.
var userId: Int = 0//Isso significa que o atributo "userId" que existe no JSON da API, será salvo automaticamente dentro dessa variável

@SerializedName("id")//Mesma lógica vista acima.
var id: Int = 0

@SerializedName("title")//Mesma lógica vista acima.
var title: String = ""

@SerializedName("body")//Mesma lógica vista acima.
var body: String = ""

}

É importante ressaltar que o nome dos nossos atributos não precisam em si conter o mesmo nome dos parâmetros que serão retornados pelo JSON.

Até porque, a anotação @SerializedName, é usada justamente para informar ao RetroFit qual parâmetro existente no JSON que será salvo em qual atributo da classe Entity.

Sendo assim, preferi nomear nossos atributos com os mesmos nomes dos parâmetro advindos do JsonPlaceHolder por questão de organização mesmo, tudo bem?

Criando o Service

Nesse segundo momento, chegou a hora de criar nossa classe de Service, que é responsável por mapear os endPoints da nossa aplicação.

Para isso, crie um novo arquivo de Interface chamado PostService.kt dentro da package retrofit:

package com.example.retrofit.retrofit

interface PostService {
}

Veja como ficou o resultado final:

package com.example.retrofit.retrofit

import retrofit2.Call
import retrofit2.http.GET

interface PostService {

@GET("posts")//Estamos dizendo que o método 'listabaixo, fará requisições do tipo GET para o endpoint 'posts'. O resto da URL que é 'https://jsonplaceholder.typicode.com/será configurado dentro do RetroFit e não aqui.
fun list(): Call<List<PostEntity>>//Preste atenção na dependência de call, pois se não for do RetroFit2, ele dá erro

}

Note que se tivéssemos outro endPoint, precisaríamos criar um novo arquivo destinado a tratar as requisições desse endPoint, como por exemplo: criar um chamado UserService.kt ou AlbumService.kt.

Como só estamos testando um retorna de lista nesse primeiro momento, usamos a anotação @GET informando o endPoint que será chamado, que no caso é "posts".

Note também que estamos retornando o Call, que nada mais é do que um método do próprio gson usado para transformar o retorno em uma lista.

Tenha em mente que o retorno está sendo setado como List<PostEntry>, nesse caso o servidor deve responder em formato de lista dessa forma: [{...}, {...}].

Caso o servidor responda diretamente (sem ser em formato de lista), como por exemplo: {...}, aí nesse caso, deveríamos usar: Call<PostEntity>.

Criando o RetroFit

Chegou o tão esperado momento em que criamos a classe do RetroFit para fazer as requisições em si.

Para isso, dentro da package retrofit, crie uma nova classe chamada de RetrofitClient.kt:

package com.example.retrofit.retrofit

class RetrofitClient {
}

No caso do RetroFit, uma boa prática é adotar um padrão de código muito utilizado, conhecido como Singleton.

Um Singleton nada mais é do que é um padrão de projeto criacional que permite a você garantir que uma classe tenha apenas uma única instância.

E vamos começar aplicando este conceito dentro dessa classe de cliente por meio do companion object:

package com.example.retrofit.retrofit

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RetrofitClient {

//No caso do RetroFit, devemos criar um Singleton para ele, para garantir que existam mais de uma única instancia dessa biblioteca.

companion object {

private lateinit var INSTANCE: Retrofit//Parte 1 do Singleton, que armazena a instancia de RetroFit
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"//Atributo que armazena a URL base. Aqui a URL sempre deve terminar em barra (/), pois se não a biblioteca da um erro

private fun getRetrofitInstance(): Retrofit {//Método responsável por retornar uma instancia do tipo RetroFit

val http = OkHttpClient.Builder()//A classe OkHttpClient ela não faz parte do RetroFit, mas é necessária pois é ela que se conecta com a internet.

if (!::INSTANCE.isInitialized) {//Verifica que já foi instanciado o RetroFit
INSTANCE = Retrofit.Builder()
.baseUrl(BASE_URL)//Método responsável por conter a URL base que iremos nos conectar
.client(http.build())//Método responsável por selecionar o cliente, que é a instancia que se comunica com a internet (OkHttpClient)
.addConverterFactory(GsonConverterFactory.create())//Método responsável por definir quem vai converter o retorno da chamada em uma classe de Entity para que possamos usar posteriormente. Quem faz isso é o Gson.
.build()//Reune as informações definidas acima e cria a comunicação em si
}

return INSTANCE//Retorna o RetroFit
}

//O método abaixo irá retornar a instancia do RetroFit de modo a acessar os serviços que foram definidos pelos Services, aqui estamos usando o conceito de Genericts, pois podem haver mais de um Service na aplicação
fun <S> createService(classService: Class<S>): S{
return getRetrofitInstance().create(classService)
}

}

}

Por se tratar de comandos fáceis de se entender, não deixe de dar uma olhada nos comentários do código feito acima, ok?

Usando o RetroFit (GET)

Com todas as 3 camadas já implementadas e configuradas, chegou o momento de colocarmos a mão na massa dentro do MainActivity.kt e fazer a nossa requisição acontecer.

Tenha em mente que diferente de outros tutoriais, aulas e conteúdos que vemos por aí na internet, aqui você não vai se deparar com TextView, RecyclerView, Listas, Campos de Textos e tudo mais.

Uma vez que o intuito deste conteúdo é mostrar da forma mais crua possível o funcionamento do RetroFit, e nós iremos fazer isso dentro do método OnCreate da MainActivity, ok?

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

//Insira aqui dentro os códigos que você vai aprender adiante...
}
}

Antes de continuar, é importante ressaltar que de forma padrão, o RetroFit faz as requisições de forma Assícrona, ou seja, ele faz a requisição de forma a não travar a aplicação.

Fazendo com que a resposta venha mais tarde, de modo que precisamos tratar isso no código posteriormente.

Para fazer com que seu código funcione de forma Síncrona, basta apenas selecionar a instancia do RetroFit e chamar a função de listagem da seguinte forma:

val service = RetrofitClient.createService(PostService::class.java)
val result = service.list()

O problema é que se o servidor demorar 10 segundos para responder, o seu aplicativo vai travar por 10 segundos. Se demorar 60 segundos, ele trava por 60 segundos.

E quando eu digo "travar", digo no sentido do usuário não conseguir interagir com a aplicação, pois ela ainda está aguardando a resposta do servidor.

Para que o RetroFit possa executar as requisições em segundo plano, você vai precisar da ajuda de uma classe chamada Call.

Pois ela é capaz de executar a operação em segundo plano através do seu método Enqueue.

Dentro do método enqueue, devemos passar uma classe herdada de CallBack, que por sua vez nos obriga a definir dois métodos: OnResponse e OnFailure.

Vejamos como isso funciona na prática:

package com.example.retrofit

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.example.retrofit.retrofit.PostEntity
import com.example.retrofit.retrofit.PostService
import com.example.retrofit.retrofit.RetrofitClient
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

//Implementação do RetroFit (Assíncrono)

//Usando o Método GET para retornar dados da requisição.

val service = RetrofitClient.createService(PostService::class.java)//Retorna a Instancia de RetrofitClient que criamos em outro arquivo, retornando o serviço que iremos utilizar (PostService)

val call: Call<List<PostEntity>> = service.list()//Aqui chamamos o método de listagem dentro da classe de Call que futuramente será responsável por chamar o método enqueue para executar a chamada em outro Thread (Background)

call.enqueue(object : Callback<List<PostEntity>>{//Estamos chamando o método Enqueue que executa a requisição em outra Thread, onde precisamos passar uma classe herdada de CallBack

//A classe herdada nos diz que precisamos implementar dois métodos, um para obter a resposta, e outro para tratar um erro caso haja.

//É importante ressaltar que um desses métodos só serão chamados quando servidor responder a requisição, isso faz com que a experiência do nosso usuário não trave.

override fun onResponse(call: Call<List<PostEntity>>, response: Response<List<PostEntity>>) {//Método chamado sempre quando existe uma resposta

if(response!= null && response.isSuccessful()) {//A resposta pode não vir aquilo que estavamos esperando, por isso é bom checar se ela é diferente de nula, e fazer o uso do método isSuccessful() para verificar se a mesma aconteceu com sucesso

//Dentro da variável "response" temos diversos respostas HTTP da URL incluíndo o corpo (body)
Log.d("Resposta: Ok", "Ok")

val list = response.body()//Pega a resposta da API que esta salva em body()

if (list!= null && list.size > 0) {//Por segurança, a resposta da API pode vir vazia, então é sempre bom fazer essas validações.

//Exemplos de como selecionar a resposta: Como a resposta se trata de uma LISTA de instâncias da classe PostEntity, podemos usar os métodos abaixo:

Log.d("Resposta ID", "${list[0].id}")//Seleciona o índice 0, retornando o id

//Podemos também fazer o uso do ForEach
list.forEach {
Log.d("Resposta", "${it.title}")
}

//Podemos também usar o For
for (item in list){
Log.d("Resposta Body", "${item.body}")
}

//Com esses dados em mãos, podemos usa-los para passar para ums RecyclerView, banco de dados e afins.

}


}

}

override fun onFailure(call: Call<List<PostEntity>>, t: Throwable) {//Método chamado quando acontece um erro na requisição
Log.d("Resposta: Fail", "$t")//Aqui estamos tratando o erro mostrando no LogCat
}

})

Olhando o código acima e seguindo os comentários ali existente, é possível compreender tudo o que esta sendo feito na requisição acima.

Lembre-se de que o corpo da mensagem sempre vem no response.body(), e por ele se tratar de uma List, podemos usar os comandos forforEach e seleção de índices para selecionar nossos itens.

Quando for testar o código acima, não deixe de dar uma olhada no console do LogCat para visualizar os retornos.

Enviando Dados (POST)

Com o RetroFit podemos enviar dados por meio do método POST.

Para cria-lo, você deverá em primeiro lugar, definir um novo método no arquivo PostService.kt, por exemplo:

@FormUrlEncoded//Neste exemplo estamos usando uma anotação do tipo FormUrlEncoded para que o RetroFit monte os parâmetros da função e os envie como application/x-www-form-urlencoded para que o servidor possa recuperar atráves do método POST
@POST("posts")//Estamos dizendo que o método postRequest abaixo, fará requisições do tipo POST para o endpoint 'posts'
fun postRequest(
@Field("title") title: String,//Usamos o @Field para declarar os parâmetros do tipo POST que serão enviados pela requisição
@Field("body") body: String,
@Field("userId") userId: Int
): Call<PostEntity>

Observe no método acima, que estamos recebendo de forma manual cada um dos campos que serão enviados na requisição usando a anotação @Field. 

Note que o retorno desse método não está usando o List, até porque não estamos retornando uma lista.

Caso o servidor responda em formato de lista, aí sim, você deveria configurar esse retorno como: Call<List<PostEntity>>

Para usar o POST, é o mesmo procedimento que vimos no GET, observe o código inserido no MainActivity.kt:

val callPost: Call<PostEntity> = service.postRequest("This is a Great Title", "Lorem Impsum dolor is awesome...", 765)//Aqui estamos recuperando a instancia do RetroFit e acessando o método POST, ao mesmo tempo que enviamos os parâmetros manualmente

callPost.enqueue(object : Callback<PostEntity> {
override fun onResponse(call: Call<PostEntity>, response: Response<PostEntity>) {
if (response != null && response.isSuccessful()) {
val list = response.body()
if (list != null) {
Log.d("Resposta POST", "${list.id}")
}
}
}

override fun onFailure(call: Call<PostEntity>, t: Throwable) {
//Método chamado quando a operação falha
Log.d("Resposta POST FAIL", "$t")
}
})

Observe no código acima que estamos tratando a resposta com o mesmo tipo de retorno visto no método postRequest, que são:

Call<PostEntity>

...

Response<PostEntity>

Ou seja, não estamos tratando a resposta como uma lista do início ao fim, como vemos no método GET.

Trabalhando com outros verbos (PUT e Delete)

No caso dos outros verbos como Put e Delete, eles funcionam de forma bem parecida com as anotações @GET e @POST que vimos anteriormente.

A diferença é que fazemos o uso das anotações @PUT e @DELETE.

No caso do @PUT, ele é usado para atualizar conteúdos e funciona da seguinte forma:

@FormUrlEncoded//Neste exemplo estamos usando uma anotação do tipo FormUrlEncoded para que o RetroFit monte os parâmetros da função e os envie como application/x-www-form-urlencoded para que o servidor possa recuperar atráves do método POST
@PUT("posts/{id}")//Estamos dizendo que o método putRequest abaixo, fará requisições do tipo POST para o endpoint 'posts/{id}', onde esse {id} será preenchido pelo valor que vier pelo parâmetro id da função abaixo.
fun putRequest(
@Path("id") id: Int,//O @Path é usado para mapear o parâmetro inserido no @PUT, fazendo com que o parâmetro id que esse método recebe, vá para ali dentro do @PUT
@Field("title") title: String,
@Field("body") body: String,
@Field("userId") userId: Int
): Call<PostEntity>

@Path é usado para mapear o parâmetro inserido no @PUT, fazendo com que o parâmetro id que esse método recebe seja referênciado dentro da URL do @PUT.

É importante ressaltar que a lógica do @Path funciona tanto para os verbos @GET@POST e @DELETE.

Veja como chamamos esse método no MainActivity.kt:

val callPut: Call<PostEntity> = service.putRequest(1, "This is a Great Title", "Lorem Impsum dolor is awesome...", 765)

callPut.enqueue(object : Callback<PostEntity> {
override fun onResponse(call: Call<PostEntity>, response: Response<PostEntity>) {
if (response != null && response.isSuccessful()) {
val list = response.body()
if (list != null) {
Log.d("Resposta PUT", "${list.title}")//Aqui vemos que a atualização ocorreu com sucesso, pois o titulo que retorna ja é o novo.
}
}
}

override fun onFailure(call: Call<PostEntity>, t: Throwable) {
//Método chamado quando a operação falha
Log.d("Resposta PUT FAIL", "$t")
}
})

Observe que estamos executando um Log.d, onde o mesmo já nos retorna o título alterado, indicando que a atualização foi um sucesso.

Já no caso do @DELETE ele funciona de forma bem parecida, vejamos:

@DELETE("posts/{id}")//Aqui estamos usando a mesma lógica dos comandos acima, só que usando o delete
fun deleteRequest(
@Path("id") id:Int
): Call<PostEntity>

Veja como chamamos este método no MainActivity.kt:

val callDelete: Call<PostEntity> = service.deleteRequest(1)

callDelete.enqueue(object : Callback<PostEntity> {
override fun onResponse(call: Call<PostEntity>, response: Response<PostEntity>) {
if (response != null && response.isSuccessful()) {
val list = response.body()
if (list != null) {
Log.d("Resposta DELETE", "${list.title}")//Apesar de ser executado o DELETE da API que estamos usando não tem qualquer tipo de retorno
}
}
}

override fun onFailure(call: Call<PostEntity>, t: Throwable) {
//Método chamado quando a operação falha
Log.d("Resposta DELETE FAIL", "$t")
}
})

Perceba que a API que estamos usando, não possui retorno para o DELETE, nesse caso o Lod.d é executado, mas o retorno vem vazio.

Fazendo o uso dos Headers

Com o RetroFit, também podemos fazer o uso de Headers usando a anotação @Headers:

@Headers(
"Accept: application/json",
"User-Agent : PostmanRuntime/7.29.0",
"Accept : */*",
"Accept-Encoding : gzip, deflate, br",
"Connection : keep-alive"
)

Eles devem ser atrelados em conjunto com os verbos @POST@PUT@GET @DELETE, veja um exemplo:

@Headers(
"Accept: application/json",
"User-Agent : PostmanRuntime/7.29.0",
"Accept : */*",
"Accept-Encoding : gzip, deflate, br",
"Connection : keep-alive"
)
@GET("posts")
fun listWithReaders(): Call<List<PostEntity>>

Como trabalhar com Retrofit em um servidor local?

Se você não dispõe de um servidor em nuvem para testar suas requisições web, saiba que também é possível enviar e receber requisições usando um servidor local.

Para isso você precisa fazer alguns passos.

1) Alterando a segurança da rede

Dentro do arquivo AndroidManifests.xml, mais especificamente dentro da tag <application...>, você vai precisar configurar um novo atributo chamado networkSecurityConfig que aponte um arquivo específico que ainda vamos criar:

<application
android:networkSecurityConfig="@xml/network_secutity_config"
android:allowBackup="true"
....

2) Criando o XML de configurações de rede

Dentro da pasta res > xml, você deverá criar um novo arquivo .xml chamado de network_secutity_config.xml, com o seguinte conteúdo:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

Esse arquivo é responsável por conter as configurações que nos possibilitam enviar e receber requisições da nossa máquina local.

3) Alterando o BASE_URL do arquivo de RetroFit

Se você acha que é só alterar a string de BASE_URL para http://localhost/ ou quem http://127.0.0.1/ você está muito enganado, pois em todas essas alternativas a requisição nunca chegará no seu servidor local.

Para isso você vai precisar apontar para um endereço específico chamado http://10.0.2.2/:

private const val BASE_URL = "http://10.0.2.2"

Com esses 3 passos você já será capaz de receber e enviar requisições na sua máquina local.

Conclusão

Neste conteúdo você aprendeu a configurar o RetroFit na sua aplicação Android de forma simples.

Até o próximo conteúdo 😄