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

  1. Não usar suspend nos métodos da interface: Sem a palavra-chave suspend, o Retrofit retorna Call<T> em vez de executar diretamente com coroutines. Sempre use suspend para integração com coroutines.

  2. Fazer requisições na Main Thread: Mesmo com coroutines, certifique-se de que o Dispatcher correto está sendo usado. O viewModelScope usa Dispatchers.Main por padrão, mas o Retrofit já muda internamente para uma thread de I/O.

  3. Não fechar o Response.errorBody(): O errorBody() é um recurso que precisa ser lido uma única vez. Leia-o imediatamente e armazene o resultado se precisar usá-lo depois.

  4. Esquecer o logging interceptor em debug: Sem o HttpLoggingInterceptor, debugar problemas de API é muito mais difícil. Configure-o em modo BODY durante o desenvolvimento.

  5. URL base sem barra final: A BASE_URL deve sempre terminar com /. Caso contrário, o Retrofit pode construir URLs incorretas ao combinar com os paths dos endpoints.

  6. 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.