Neste tutorial, você vai aprender a usar o Retrofit com Kotlin para consumir APIs REST no Android. Vamos cobrir desde a configuração inicial até tópicos avançados como integração com Coroutines, interceptors do OkHttp, tratamento robusto de erros e boas práticas de arquitetura. O Retrofit é a biblioteca mais popular para requisições HTTP no ecossistema Android e dominar seu uso é fundamental para qualquer desenvolvedor.
O que é o Retrofit?
O Retrofit é uma biblioteca da Square que transforma interfaces Kotlin/Java em clientes HTTP. Você define os endpoints da API como métodos de uma interface, e o Retrofit gera automaticamente a implementação. Ele trabalha em conjunto com o OkHttp para gerenciar as requisições e com conversores como Gson ou Moshi para serializar/desserializar JSON.
Passo 1: Configurando as Dependências
Adicione as dependências no build.gradle.kts:
dependencies {
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Conversor Gson (opção 1)
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// OU Conversor Moshi (opção 2 — recomendado para Kotlin)
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
// OkHttp Logging Interceptor
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
Não esqueça de adicionar a permissão de internet no AndroidManifest.xml:
// No AndroidManifest.xml, adicione:
// <uses-permission android:name="android.permission.INTERNET" />
Passo 2: Definindo os Modelos de Dados
Vamos criar os modelos para uma API de posts (como a JSONPlaceholder). Com Moshi, usamos a anotação @JsonClass:
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Post(
val id: Int,
@Json(name = "user_id") val userId: Int,
val title: String,
val body: String
)
@JsonClass(generateAdapter = true)
data class Usuario(
val id: Int,
val name: String,
val email: String,
@Json(name = "phone") val telefone: String
)
// Com Gson, basta usar @SerializedName:
// data class Post(
// val id: Int,
// @SerializedName("user_id") val userId: Int,
// val title: String,
// val body: String
// )
A vantagem do Moshi com codegen é que ele gera adapters em tempo de compilação, evitando reflexão em runtime — o que resulta em melhor performance e compatibilidade com ProGuard/R8.
Passo 3: Definindo a Interface da API
Crie uma interface com os endpoints da API. Com Kotlin e Coroutines, os métodos podem ser suspend:
import retrofit2.Response
import retrofit2.http.*
interface ApiService {
@GET("posts")
suspend fun listarPosts(): List<Post>
@GET("posts/{id}")
suspend fun buscarPost(@Path("id") id: Int): Post
@GET("posts")
suspend fun buscarPostsPorUsuario(
@Query("userId") userId: Int
): List<Post>
@GET("posts")
suspend fun buscarPostsPaginados(
@Query("_page") pagina: Int,
@Query("_limit") limite: Int = 20
): Response<List<Post>> // Response para acessar headers e código HTTP
@POST("posts")
suspend fun criarPost(@Body post: Post): Post
@PUT("posts/{id}")
suspend fun atualizarPost(
@Path("id") id: Int,
@Body post: Post
): Post
@PATCH("posts/{id}")
suspend fun atualizarParcialmente(
@Path("id") id: Int,
@Body campos: Map<String, @JvmSuppressWildcards Any>
): Post
@DELETE("posts/{id}")
suspend fun deletarPost(@Path("id") id: Int): Response<Unit>
@GET("users/{id}")
suspend fun buscarUsuario(
@Path("id") id: Int,
@Header("Authorization") token: String
): Usuario
}
As anotações @GET, @POST, @PUT, @DELETE definem o método HTTP. @Path substitui variáveis na URL, @Query adiciona parâmetros de query string, @Body envia o corpo da requisição, e @Header adiciona cabeçalhos específicos.
Passo 4: Configurando o Retrofit com OkHttp
Agora criamos a instância do Retrofit com todas as configurações necessárias:
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitClient {
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
// Configuração do Moshi
private val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
// Logging Interceptor — mostra requisições no Logcat
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
// Configuração do OkHttp
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
// Instância do Retrofit
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
O HttpLoggingInterceptor com nível BODY mostra URLs, headers e corpo das requisições e respostas no Logcat — extremamente útil durante o desenvolvimento. Em produção, use NONE para não expor dados sensíveis.
Passo 5: Interceptors Personalizados
Interceptors do OkHttp permitem modificar todas as requisições de forma centralizada. Isso é muito útil para adicionar headers de autenticação:
import okhttp3.Interceptor
import okhttp3.Response
class AuthInterceptor(
private val tokenProvider: () -> String?
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = tokenProvider()
if (token == null) {
return chain.proceed(originalRequest)
}
val requestComAuth = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.header("Accept", "application/json")
.build()
return chain.proceed(requestComAuth)
}
}
// Interceptor para retry automático
class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var tentativa = 0
var response: Response? = null
while (tentativa < maxRetries) {
try {
response?.close()
response = chain.proceed(chain.request())
if (response.isSuccessful) return response
} catch (e: Exception) {
if (tentativa == maxRetries - 1) throw e
}
tentativa++
}
return response ?: throw IllegalStateException("Todas as tentativas falharam")
}
}
// Adicione os interceptors ao OkHttpClient:
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor { SessionManager.getToken() })
.addInterceptor(loggingInterceptor) // logging sempre por último
.build()
Passo 6: Tratamento Robusto de Erros
Requisições HTTP podem falhar de diversas formas. Vamos criar uma estrutura robusta para lidar com esses cenários:
sealed class Resultado<out T> {
data class Sucesso<T>(val dados: T) : Resultado<T>()
data class Erro(val mensagem: String, val codigo: Int? = null) : Resultado<Nothing>()
data object Carregando : Resultado<Nothing>()
}
suspend fun <T> chamarApi(chamada: suspend () -> T): Resultado<T> {
return try {
Resultado.Sucesso(chamada())
} catch (e: retrofit2.HttpException) {
val mensagem = when (e.code()) {
400 -> "Requisição inválida"
401 -> "Não autorizado — faça login novamente"
403 -> "Acesso negado"
404 -> "Recurso não encontrado"
500 -> "Erro interno do servidor"
else -> "Erro HTTP: ${e.code()}"
}
Resultado.Erro(mensagem, e.code())
} catch (e: java.net.UnknownHostException) {
Resultado.Erro("Sem conexão com a internet")
} catch (e: java.net.SocketTimeoutException) {
Resultado.Erro("Tempo de conexão esgotado")
} catch (e: Exception) {
Resultado.Erro("Erro inesperado: ${e.localizedMessage}")
}
}
Usando no Repository:
class PostRepository(private val api: ApiService) {
suspend fun listarPosts(): Resultado<List<Post>> {
return chamarApi { api.listarPosts() }
}
suspend fun buscarPost(id: Int): Resultado<Post> {
return chamarApi { api.buscarPost(id) }
}
suspend fun criarPost(post: Post): Resultado<Post> {
return chamarApi { api.criarPost(post) }
}
}
E no ViewModel:
class PostViewModel(private val repository: PostRepository) : ViewModel() {
private val _posts = MutableStateFlow<Resultado<List<Post>>>(Resultado.Carregando)
val posts: StateFlow<Resultado<List<Post>>> = _posts.asStateFlow()
fun carregarPosts() {
viewModelScope.launch {
_posts.value = Resultado.Carregando
_posts.value = repository.listarPosts()
}
}
}
Passo 7: Trabalhando com Response para Metadados
Às vezes você precisa acessar headers, códigos de status ou verificar se a resposta foi bem-sucedida. Use Response<T>:
suspend fun carregarPostsPaginados(pagina: Int): Resultado<List<Post>> {
return try {
val response = api.buscarPostsPaginados(pagina)
if (response.isSuccessful) {
val totalPaginas = response.headers()["X-Total-Count"]?.toIntOrNull()
val posts = response.body() ?: emptyList()
Resultado.Sucesso(posts)
} else {
val errorBody = response.errorBody()?.string()
Resultado.Erro("Erro: ${response.code()} - $errorBody", response.code())
}
} catch (e: Exception) {
Resultado.Erro("Falha na requisição: ${e.message}")
}
}
Erros Comuns
Não usar
suspendnos métodos da interface: Sem a palavra-chave suspend, o Retrofit retornaCall<T>em vez de executar diretamente com coroutines. Sempre usesuspendpara integração com coroutines.Fazer requisições na Main Thread: Mesmo com coroutines, certifique-se de que o Dispatcher correto está sendo usado. O
viewModelScopeusaDispatchers.Mainpor padrão, mas o Retrofit já muda internamente para uma thread de I/O.Não fechar o
Response.errorBody(): OerrorBody()é um recurso que precisa ser lido uma única vez. Leia-o imediatamente e armazene o resultado se precisar usá-lo depois.Esquecer o logging interceptor em debug: Sem o
HttpLoggingInterceptor, debugar problemas de API é muito mais difícil. Configure-o em modoBODYdurante o desenvolvimento.URL base sem barra final: A
BASE_URLdeve sempre terminar com/. Caso contrário, o Retrofit pode construir URLs incorretas ao combinar com os paths dos endpoints.Não tratar diferentes tipos de erro: Tratar todos os erros como genéricos dificulta a experiência do usuário. Diferencie entre erros de rede, erros HTTP e erros de parsing.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a utilizar o Retrofit com Kotlin de forma completa e profissional: configuração com Gson ou Moshi, definição de interfaces de API com suporte a Coroutines, interceptors personalizados para autenticação e logging, tratamento robusto de erros com sealed classes, e acesso a metadados de resposta HTTP.
Como próximos passos, recomendamos:
- Integrar Retrofit com Room Database para cache offline de dados
- Implementar a arquitetura MVVM completa com Repository pattern
- Explorar Kotlin Flow para transformar respostas de API em streams reativos
- Consultar o glossário de interface e coroutine para reforçar conceitos
- Estudar autenticação OAuth2 com refresh token automático usando interceptors
O Retrofit combinado com Kotlin e Coroutines oferece uma experiência de desenvolvimento moderna e eficiente para qualquer projeto Android que precise consumir APIs REST.