---
title: "Retrofit com Kotlin em 2026: Tutorial Android com OkHttp, Coroutines e Cache | Kotlin Brasil"
url: "https://kotlin.dev.br/tutoriais/kotlin-retrofit-tutorial/"
markdown_url: "https://kotlin.dev.br/tutoriais/kotlin-retrofit-tutorial.MD"
description: "Aprenda Retrofit com Kotlin em apps Android: setup, OkHttp, coroutines, interceptors, autenticação, erros, cache offline e boas práticas de produção."
date: "2025-07-10"
author: "Karina Melo"
---

# Retrofit com Kotlin em 2026: Tutorial Android com OkHttp, Coroutines e Cache | Kotlin Brasil

Aprenda Retrofit com Kotlin em apps Android: setup, OkHttp, coroutines, interceptors, autenticação, erros, cache offline e boas práticas de produção.


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](/glossario/coroutine/), 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](/tutoriais/kotlin-room-database-tutorial/) ou [DataStore](/tutoriais/datastore-preferences-kotlin/). 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](/glossario/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`:

```kotlin
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`:

```kotlin
// 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`:

```kotlin
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](/glossario/interface/) com os endpoints da API. Com Kotlin e Coroutines, os métodos podem ser [suspend](/glossario/suspend/):

```kotlin
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:

```kotlin
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:

```kotlin
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:

```kotlin
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:

```kotlin
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:

```kotlin
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](/tutoriais/sealed-classes-tutorial/) e com estado de tela em [MVVM](/tutoriais/kotlin-mvvm-tutorial/). Em vez de espalhar `try/catch` pela Activity, concentre a tradução no data source ou repository.

Usando no Repository:

```kotlin
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:

```kotlin
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>`:

```kotlin
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](/blog/android-offline-first-kotlin-2026/).

Um fluxo comum fica assim:

```kotlin
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](/blog/workmanager-kotlin-android-2026/). 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:

```kotlin
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](/blog/rate-limiting-kotlin-spring-ktor-2026/), 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:

```kotlin
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](/blog/spring-security-kotlin-jwt-oauth2-2026/). No app, complemente isso com uma política explícita de [segurança de dados locais no Android](/blog/seguranca-dados-locais-android-kotlin-2026/) para tokens, cache HTTP, logs e logout.

## Erros Comuns

1. **Não usar `suspend` nos métodos da interface**: Sem a palavra-chave [suspend](/glossario/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.

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

8. **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.

9. **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.

10. **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](/glossario/coroutine/), 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](/tutoriais/kotlin-room-database-tutorial/) para cache offline de dados
- Implementar a arquitetura [MVVM](/tutoriais/kotlin-mvvm-tutorial/) completa com Repository pattern
- Explorar [Kotlin Flow](/tutoriais/kotlin-flow-tutorial/) para transformar respostas de API em streams reativos
- Estudar [Android offline-first com Kotlin](/blog/android-offline-first-kotlin-2026/) para sincronização resiliente
- Usar [WorkManager com Kotlin](/blog/workmanager-kotlin-android-2026/) para retry em background quando a rede voltar
- Consultar o [glossário de interface](/glossario/interface/) e [coroutine](/glossario/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 <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go para APIs de alta performance</a> e <a href="https://python.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python com FastAPI para prototipagem rápida</a>.
