Neste tutorial, você vai aprender a usar Retrofit com Kotlin para consumir APIs REST no Android em 2026. Vamos cobrir desde a configuração inicial até decisões de produção: integração com coroutines, OkHttp, interceptors, autenticação, tratamento robusto de erros, cache local, integração com repository e cuidados para apps que precisam funcionar em redes instáveis.
Retrofit continua aparecendo em vagas Android porque resolve um problema central: transformar contratos HTTP em interfaces Kotlin testáveis. Em projetos reais, porém, não basta criar uma interface e chamar api.buscarDados(). O app precisa lidar com timeout, token expirado, resposta vazia, erro 401, retry, paginação, logs sem dados sensíveis e sincronização com Room ou DataStore. Este guia atualiza o tutorial para essa realidade.
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, é 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.11.0")
// Conversor Gson (opção 1)
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
// OU Conversor Moshi (opção 2 — recomendado para Kotlin)
implementation("com.squareup.retrofit2:converter-moshi:2.11.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.8.1")
}
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. Gson ainda funciona, mas Moshi ou kotlinx.serialization costumam combinar melhor com modelos Kotlin imutáveis, campos nulos explícitos e projetos que usam R8 de forma agressiva.
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.
Em apps profissionais, também vale configurar timeouts menores para telas interativas e maiores para uploads ou downloads. Um feed pode falhar rápido e mostrar dados locais; um upload de foto talvez precise de outra estratégia. Não trate todos os endpoints como se tivessem o mesmo custo para o usuário.
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()
Para tokens OAuth2 com refresh automático, evite fazer a renovação dentro de qualquer interceptor sem controle. O OkHttp oferece Authenticator, que roda quando o servidor responde 401 e permite tentar uma nova requisição com token renovado. Isso reduz duplicação e evita que cada repository precise conhecer detalhes de autenticação:
class TokenAuthenticator(
private val sessao: SessaoRepository,
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (responseCount(response) >= 2) return null
val novoToken = runBlocking {
sessao.renovarTokenSePossivel()
} ?: return null
return response.request.newBuilder()
.header("Authorization", "Bearer $novoToken")
.build()
}
private fun responseCount(response: Response): Int {
var count = 1
var prior = response.priorResponse
while (prior != null) {
count++
prior = prior.priorResponse
}
return count
}
}
O exemplo usa runBlocking porque a API do OkHttp é síncrona. Em produção, mantenha esse caminho curto, protegido contra deadlock e sem chamadas desnecessárias. Se o refresh token falhar, retorne null, limpe a sessão no repository e deixe a UI conduzir o usuário para login.
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}")
}
}
Esse wrapper é suficiente para tutoriais, mas apps maiores normalmente separam erro de rede, erro de autenticação, erro de validação e erro inesperado. Assim, a UI consegue tomar decisões melhores: mostrar botão de tentar novamente, pedir login, destacar campo inválido ou exibir dados em cache. A regra prática é não perder informação cedo demais.
Uma versão mais expressiva pode incluir uma hierarquia de falhas:
sealed interface FalhaApi {
data object SemInternet : FalhaApi
data object Timeout : FalhaApi
data object NaoAutorizado : FalhaApi
data class Http(val codigo: Int, val corpo: String?) : FalhaApi
data class Parsing(val detalhe: String?) : FalhaApi
}
Essa modelagem conversa bem com sealed classes e com estado de tela em MVVM. Em vez de espalhar try/catch pela Activity, concentre a tradução no data source ou repository.
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}")
}
}
Retrofit, Room e cache offline
Uma tela Android moderna raramente deve depender apenas da resposta imediata da API. Em conexões móveis instáveis, o melhor desenho é fazer a UI observar dados locais e deixar o repository decidir quando buscar rede. Essa é a base de uma arquitetura Android offline-first com Kotlin.
Um fluxo comum fica assim:
class ProdutosRepository(
private val api: ProdutosApi,
private val dao: ProdutoDao,
) {
fun observarProdutos(): Flow<List<Produto>> =
dao.observarTodos().map { entidades ->
entidades.map { it.toDomain() }
}
suspend fun atualizarProdutos() {
val remotos = api.listarProdutos()
dao.substituirTodos(remotos.map { it.toEntity() })
}
}
Nesse modelo, a tela não precisa saber se os dados vieram da internet ou do banco local. Ela observa o Flow do Room e chama atualizarProdutos() em momentos controlados: pull-to-refresh, abertura da tela, retorno de conectividade ou execução em background com WorkManager. O usuário vê dados disponíveis rapidamente, enquanto a sincronização acontece sem bloquear a experiência.
Para cache HTTP puro, OkHttp também oferece cache de resposta, mas ele não substitui persistência local de produto. Use cache HTTP para reduzir chamadas repetidas quando os headers do servidor ajudam. Use Room quando a experiência precisa continuar funcionando, quando há filtros locais, favoritos, fila de operações pendentes ou estado que precisa sobreviver a limpeza de memória.
Paginação, retry e limites práticos
APIs reais crescem. Em vez de baixar tudo de uma vez, use paginação por página, cursor ou nextToken. Retrofit não impõe um padrão; você modela o contrato:
data class PaginaProdutos(
val itens: List<ProdutoDto>,
val proximoCursor: String?,
)
interface ProdutosApi {
@GET("produtos")
suspend fun listarProdutos(
@Query("cursor") cursor: String? = null,
@Query("limite") limite: Int = 30,
): PaginaProdutos
}
Retry também precisa de critério. Não faça retry automático para qualquer erro dentro de um interceptor genérico. Erros 408, 429 e 5xx podem ser temporários; 400, 401, 403 e 404 geralmente exigem ação diferente. Para operações com efeito colateral, como criar pedido ou confirmar pagamento, use chave idempotente antes de repetir chamada. Sem isso, uma tentativa duplicada pode criar dados duplicados no servidor.
Quando o backend informa Retry-After, respeite esse header. Se a API aplica rate limiting, insistir imediatamente piora a experiência e pode bloquear o usuário por mais tempo.
Segurança e observabilidade
Retrofit e OkHttp facilitam logs, mas logs podem vazar token, CPF, email, endereço ou payload sensível. Em debug, BODY ajuda. Em produção, prefira logs estruturados sem corpo de resposta, com request id, endpoint lógico, status HTTP, duração e tipo de falha. Se o backend devolve um header como X-Request-Id, propague esse valor para facilitar investigação.
Também vale padronizar headers:
class AppHeadersInterceptor(
private val versaoApp: String,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("Accept", "application/json")
.header("X-App-Version", versaoApp)
.build()
return chain.proceed(request)
}
}
Esse tipo de header ajuda o backend a diagnosticar bugs por versão, bloquear clientes antigos com mais clareza e correlacionar falhas. Para autenticação mais profunda no servidor, veja também Spring Security com Kotlin, JWT e OAuth2. No app, complemente isso com uma política explícita de segurança de dados locais no Android para tokens, cache HTTP, logs e logout.
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.
Colocar regra de negócio no interceptor: interceptor deve cuidar de transporte, headers e autenticação transversal. Regra de produto pertence ao repository, use case ou camada de domínio.
Confiar apenas na API para telas críticas: se a tela precisa abrir em rede ruim, combine Retrofit com Room, Flow e estratégia offline-first.
Fazer retry de POST sem idempotência: repetir uma operação de escrita pode duplicar pedido, mensagem ou transação. Use identificador idempotente ou confirme o estado antes de reenviar.
Logar dados sensíveis: logging de corpo em produção pode expor tokens e informações pessoais. Restrinja logs detalhados ao debug.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a utilizar Retrofit com Kotlin de forma completa e profissional: configuração com Gson ou Moshi, definição de interfaces de API com suporte a coroutines, OkHttp, interceptors personalizados, autenticação, tratamento robusto de erros com sealed classes, acesso a metadados HTTP, cache offline com Room e cuidados de produção.
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
- Estudar Android offline-first com Kotlin para sincronização resiliente
- Usar WorkManager com Kotlin para retry em background quando a rede voltar
- 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. Para desenvolvimento de APIs no servidor, considere também Go para APIs de alta performance e Python com FastAPI para prototipagem rápida.