O que é Retrofit e como ele se encaixa no app
Retrofit é uma biblioteca para consumo de APIs HTTP que transforma endpoints REST em funções Kotlin. Ele trabalha em conjunto com o OkHttp (cliente HTTP) e com um conversor (Moshi ou Gson) para transformar JSON em objetos Kotlin. Em um app bem organizado, o Retrofit fica na camada de dados (data), dentro de um remote datasource e/ou repositório, e expõe funções suspend para serem chamadas por use cases ou diretamente pelo repositório.
Componentes principais
- OkHttpClient: configura timeouts, cache, interceptors, headers e logs.
- Retrofit: define baseUrl, conversor e cria a interface de API.
- Converter (Moshi/Gson): faz parsing do JSON para DTOs.
- DTO (Data Transfer Object): modelo que representa o JSON da API.
- Modelo de domínio: modelo usado no app (UI/negócio), independente do formato da API.
Dependências (Gradle) para Retrofit, OkHttp e conversor
Escolha Moshi ou Gson. Moshi costuma ser uma escolha moderna e segura com Kotlin. Abaixo um exemplo com Moshi.
// build.gradle (Module: app) - trechos relevantes
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.moshi:moshi:1.15.1")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
}
Se preferir Gson, substitua o conversor e remova Moshi:
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.google.code.gson:gson:2.11.0")
Configuração do OkHttp: timeouts, logs, headers e cache
Timeouts
Timeouts evitam que o app fique esperando indefinidamente. Ajuste conforme o tipo de API e rede esperada.
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
Interceptor de logs (útil em debug)
O HttpLoggingInterceptor imprime request/response no Logcat. Use nível BODY apenas em debug para evitar vazamento de dados em produção.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
import okhttp3.logging.HttpLoggingInterceptor
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
Interceptor para headers (ex.: token, idioma, user-agent)
Interceptors permitem alterar a requisição antes de enviá-la. Um caso comum é adicionar Authorization e Accept.
import okhttp3.Interceptor
class HeadersInterceptor(
private val tokenProvider: () -> String?
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val original = chain.request()
val token = tokenProvider()
val request = original.newBuilder()
.header("Accept", "application/json")
.apply {
if (!token.isNullOrBlank()) {
header("Authorization", "Bearer $token")
}
}
.build()
return chain.proceed(request)
}
}
Cache HTTP básico (quando a API permite)
OkHttp suporta cache baseado em headers HTTP (Cache-Control, ETag). Para funcionar bem, o servidor precisa cooperar. Ainda assim, você pode adicionar um cache local e, opcionalmente, forçar comportamento em offline.
import okhttp3.Cache
import java.io.File
fun provideOkHttpCache(cacheDir: File): Cache {
val cacheSize = 10L * 1024L * 1024L // 10 MB
return Cache(File(cacheDir, "http_cache"), cacheSize)
}
val okHttpClient = OkHttpClient.Builder()
.cache(provideOkHttpCache(context.cacheDir))
.build()
Se você quiser um cache “mais agressivo” para listas (ex.: 60s), pode adicionar um interceptor que injeta Cache-Control na resposta. Use com cuidado, pois pode mostrar dados desatualizados.
import okhttp3.Interceptor
class CacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val response = chain.proceed(chain.request())
return response.newBuilder()
.header("Cache-Control", "public, max-age=60")
.build()
}
}
Configuração do Retrofit com Moshi (parsing)
O Retrofit precisa de uma baseUrl (terminando com /) e de um conversor. Com Moshi, é comum adicionar o KotlinJsonAdapterFactory para lidar melhor com data classes.
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
Com Gson, a ideia é a mesma, trocando o converter:
import retrofit2.converter.gson.GsonConverterFactory
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
Modelando DTOs e mapeando para modelos de domínio
DTOs devem refletir o JSON da API (nomes, campos opcionais, estruturas). Já o domínio deve refletir o que o app precisa para exibir e tomar decisões, com nomes mais claros e tipos mais convenientes.
Prática com API pública: GitHub Search
Vamos consumir a API pública do GitHub para buscar repositórios e abrir detalhes. Endpoint:
GET /search/repositories?q=kotlin&page=1&per_page=20GET /repos/{owner}/{repo}
Exemplo de JSON (simplificado) do search:
{
"total_count": 123,
"items": [
{
"id": 1,
"full_name": "JetBrains/kotlin",
"description": "...",
"stargazers_count": 100,
"owner": { "login": "JetBrains", "avatar_url": "..." }
}
]
}
DTOs (Moshi)
import com.squareup.moshi.Json
data class SearchReposResponseDto(
@Json(name = "total_count") val totalCount: Int,
val items: List<RepoDto>
)
data class RepoDto(
val id: Long,
@Json(name = "full_name") val fullName: String,
val description: String?,
@Json(name = "stargazers_count") val stars: Int,
val owner: OwnerDto
)
data class OwnerDto(
val login: String,
@Json(name = "avatar_url") val avatarUrl: String
)
DTO para detalhe (pode ser mais completo, mas foque no necessário):
data class RepoDetailDto(
val id: Long,
@Json(name = "full_name") val fullName: String,
val description: String?,
@Json(name = "stargazers_count") val stars: Int,
@Json(name = "forks_count") val forks: Int,
@Json(name = "open_issues_count") val openIssues: Int,
val owner: OwnerDto
)
Modelos de domínio
No domínio, você pode simplificar nomes e agrupar o que a UI precisa.
data class Repo(
val id: Long,
val name: String,
val ownerLogin: String,
val ownerAvatarUrl: String,
val description: String,
val stars: Int
)
data class RepoDetail(
val id: Long,
val name: String,
val ownerLogin: String,
val ownerAvatarUrl: String,
val description: String,
val stars: Int,
val forks: Int,
val openIssues: Int
)
Mapeamento DTO -> Domínio
Crie funções de extensão para manter o código limpo e testável.
fun RepoDto.toDomain(): Repo = Repo(
id = id,
name = fullName,
ownerLogin = owner.login,
ownerAvatarUrl = owner.avatarUrl,
description = description.orEmpty(),
stars = stars
)
fun RepoDetailDto.toDomain(): RepoDetail = RepoDetail(
id = id,
name = fullName,
ownerLogin = owner.login,
ownerAvatarUrl = owner.avatarUrl,
description = description.orEmpty(),
stars = stars,
forks = forks,
openIssues = openIssues
)
Definindo a interface da API no Retrofit
Retrofit usa anotações para descrever endpoints, query params e path params. Com coroutines, você define funções suspend.
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface GitHubApi {
@GET("search/repositories")
suspend fun searchRepositories(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") perPage: Int
): SearchReposResponseDto
@GET("repos/{owner}/{repo}")
suspend fun getRepositoryDetail(
@Path("owner") owner: String,
@Path("repo") repo: String
): RepoDetailDto
}
Crie a instância:
val api = retrofit.create(GitHubApi::class.java)
Chamadas assíncronas com coroutines e tratamento de erros
Quando você chama uma função suspend do Retrofit, ela executa a request fora da thread principal (desde que você esteja usando coroutines corretamente). O principal ponto é lidar com falhas de rede, HTTP e parsing.
Erros comuns
- IOException: sem internet, timeout, falha de conexão.
- HttpException: status HTTP 4xx/5xx (quando você usa
Response<T>ou quando o Retrofit lança em certas configurações). - JsonDataException / JsonEncodingException (Moshi) ou JsonSyntaxException (Gson): JSON inesperado.
Preferindo Response<T> para controlar status e body
Você pode retornar Response<T> e decidir como tratar cada caso, especialmente útil para mensagens de erro e paginação.
import retrofit2.Response
interface GitHubApi {
@GET("search/repositories")
suspend fun searchRepositories(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") perPage: Int
): Response<SearchReposResponseDto>
}
Exemplo de chamada no repositório:
import retrofit2.HttpException
import java.io.IOException
sealed class AppError {
data class Network(val message: String) : AppError()
data class Http(val code: Int, val message: String) : AppError()
data class Parsing(val message: String) : AppError()
data class Unknown(val message: String) : AppError()
}
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
}
class ReposRepository(
private val api: GitHubApi
) {
suspend fun search(query: String, page: Int, perPage: Int): Result<List<Repo>> {
return try {
val response = api.searchRepositories(query, page, perPage)
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
Result.Failure(AppError.Parsing("Resposta vazia"))
} else {
Result.Success(body.items.map { it.toDomain() })
}
} else {
Result.Failure(AppError.Http(response.code(), response.message()))
}
} catch (e: IOException) {
Result.Failure(AppError.Network(e.message ?: "Falha de rede"))
} catch (e: HttpException) {
Result.Failure(AppError.Http(e.code(), e.message()))
} catch (e: Exception) {
Result.Failure(AppError.Unknown(e.message ?: "Erro inesperado"))
}
}
}
Paginação simples (page/per_page) e estado de lista
Muitas APIs usam paginação por página. A estratégia mais simples:
- Manter
currentPageeisLoading. - Carregar a primeira página ao iniciar ou ao mudar a busca.
- Carregar próxima página quando o usuário chega perto do fim da lista.
- Parar quando vier uma página vazia (ou quando
total_countindicar fim).
Estrutura de estado para a lista
data class ReposListState(
val query: String = "kotlin",
val items: List<Repo> = emptyList(),
val page: Int = 1,
val isLoading: Boolean = false,
val endReached: Boolean = false,
val errorMessage: String? = null
)
Funções de carregar primeira página e próxima página
Exemplo de lógica (pode ficar em ViewModel, mas aqui focamos na regra):
private const val PER_PAGE = 20
suspend fun loadFirstPage(
repository: ReposRepository,
state: ReposListState
): ReposListState {
val newState = state.copy(isLoading = true, errorMessage = null, page = 1, endReached = false)
return when (val result = repository.search(newState.query, 1, PER_PAGE)) {
is Result.Success -> {
val end = result.data.isEmpty()
newState.copy(items = result.data, isLoading = false, endReached = end)
}
is Result.Failure -> newState.copy(isLoading = false, errorMessage = result.error.toString())
}
}
suspend fun loadNextPage(
repository: ReposRepository,
state: ReposListState
): ReposListState {
if (state.isLoading || state.endReached) return state
val nextPage = state.page + 1
val loadingState = state.copy(isLoading = true, errorMessage = null)
return when (val result = repository.search(state.query, nextPage, PER_PAGE)) {
is Result.Success -> {
val newItems = state.items + result.data
val end = result.data.isEmpty()
loadingState.copy(items = newItems, page = nextPage, isLoading = false, endReached = end)
}
is Result.Failure -> loadingState.copy(isLoading = false, errorMessage = result.error.toString())
}
}
Na UI, o gatilho de paginação pode ser: ao renderizar a lista, quando o usuário chega nos últimos itens (ex.: índice >= items.size - 5), chamar loadNextPage.
Prática completa: lista e detalhe consumindo GitHub API
1) Criar o módulo de rede (NetworkModule simples)
Sem entrar em frameworks de DI, você pode criar um objeto/fábrica para centralizar a criação do Retrofit e API.
object NetworkModule {
fun createOkHttpClient(
tokenProvider: () -> String?,
cache: okhttp3.Cache? = null,
isDebug: Boolean = true
): okhttp3.OkHttpClient {
val logging = okhttp3.logging.HttpLoggingInterceptor().apply {
level = if (isDebug) okhttp3.logging.HttpLoggingInterceptor.Level.BODY
else okhttp3.logging.HttpLoggingInterceptor.Level.NONE
}
return okhttp3.OkHttpClient.Builder()
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
.apply { if (cache != null) cache(cache) }
.addInterceptor(HeadersInterceptor(tokenProvider))
.addInterceptor(logging)
.build()
}
fun createRetrofit(okHttpClient: okhttp3.OkHttpClient): retrofit2.Retrofit {
val moshi = com.squareup.moshi.Moshi.Builder()
.add(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory())
.build()
return retrofit2.Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(okHttpClient)
.addConverterFactory(retrofit2.converter.moshi.MoshiConverterFactory.create(moshi))
.build()
}
fun createGitHubApi(retrofit: retrofit2.Retrofit): GitHubApi =
retrofit.create(GitHubApi::class.java)
}
2) Repositório com busca e detalhe
class ReposRepository(
private val api: GitHubApi
) {
suspend fun search(query: String, page: Int, perPage: Int): Result<List<Repo>> {
return try {
val response = api.searchRepositories(query, page, perPage)
if (!response.isSuccessful) {
return Result.Failure(AppError.Http(response.code(), response.message()))
}
val body = response.body() ?: return Result.Failure(AppError.Parsing("Resposta vazia"))
Result.Success(body.items.map { it.toDomain() })
} catch (e: java.io.IOException) {
Result.Failure(AppError.Network(e.message ?: "Falha de rede"))
} catch (e: Exception) {
Result.Failure(AppError.Unknown(e.message ?: "Erro inesperado"))
}
}
suspend fun getDetail(owner: String, repo: String): Result<RepoDetail> {
return try {
val dto = api.getRepositoryDetail(owner, repo)
Result.Success(dto.toDomain())
} catch (e: java.io.IOException) {
Result.Failure(AppError.Network(e.message ?: "Falha de rede"))
} catch (e: Exception) {
Result.Failure(AppError.Unknown(e.message ?: "Erro inesperado"))
}
}
}
Note que getRepositoryDetail aqui retorna diretamente o DTO (sem Response<T>). Se quiser tratar status HTTP explicitamente, mude para Response<RepoDetailDto> como feito na busca.
3) Extraindo owner/repo a partir do fullName
O endpoint de detalhe precisa de owner e repo. Se você guardou full_name como JetBrains/kotlin, pode separar:
fun splitFullName(fullName: String): Pair<String, String> {
val parts = fullName.split("/")
require(parts.size == 2)
return parts[0] to parts[1]
}
4) Exibindo lista e abrindo detalhe (fluxo de dados)
Na tela de lista, você carrega a primeira página e renderiza Repo. Ao clicar em um item, navega para a tela de detalhe passando fullName (ou owner/repo). Na tela de detalhe, chama getDetail e renderiza RepoDetail.
| Tela | Entrada | Chamada | Saída |
|---|---|---|---|
| Lista | query, page | searchRepositories | List<Repo> |
| Detalhe | owner, repo | getRepositoryDetail | RepoDetail |
Interceptors na prática: autenticação opcional e rate limit
A API do GitHub tem limites de requisição. Se você usar um token (personal access token), o limite melhora. O HeadersInterceptor mostrado antes permite inserir o token sem espalhar lógica pela base.
Uma estratégia simples é armazenar o token em uma fonte segura (ex.: armazenamento seguro) e fornecer via tokenProvider. Em ambientes de teste, você pode retornar null para não enviar header.
Parsing e compatibilidade com campos opcionais
APIs mudam e nem sempre entregam todos os campos. Para reduzir crashes:
- Marque campos como anuláveis no DTO quando não forem garantidos (
String?,Int?). - Use
orEmpty()ou valores padrão no mapeamento para domínio. - Evite usar o DTO diretamente na UI; o domínio pode garantir invariantes (ex.:
descriptionnunca nula).
Exemplo com valor padrão no DTO (quando fizer sentido):
data class RepoDto(
val id: Long,
@Json(name = "full_name") val fullName: String,
val description: String? = null,
@Json(name = "stargazers_count") val stars: Int = 0,
val owner: OwnerDto
)
Cache básico: quando usar e como aplicar sem complicar
Existem dois tipos comuns de cache:
- Cache HTTP (OkHttp): automático, depende de headers do servidor. Bom para reduzir tráfego e acelerar.
- Cache de aplicação: você guarda o último resultado em memória (ou persistência) para reuso rápido.
Cache em memória para detalhe (simples e útil)
Detalhes de um repositório mudam pouco. Um cache em memória evita múltiplas chamadas ao abrir/voltar rapidamente.
class RepoDetailCache {
private val map = mutableMapOf<String, RepoDetail>()
fun get(key: String): RepoDetail? = map[key]
fun put(key: String, value: RepoDetail) { map[key] = value }
}
class ReposRepository(
private val api: GitHubApi,
private val detailCache: RepoDetailCache
) {
suspend fun getDetail(owner: String, repo: String): Result<RepoDetail> {
val key = "$owner/$repo"
detailCache.get(key)?.let { return Result.Success(it) }
return try {
val dto = api.getRepositoryDetail(owner, repo)
val domain = dto.toDomain()
detailCache.put(key, domain)
Result.Success(domain)
} catch (e: java.io.IOException) {
Result.Failure(AppError.Network(e.message ?: "Falha de rede"))
} catch (e: Exception) {
Result.Failure(AppError.Unknown(e.message ?: "Erro inesperado"))
}
}
}
Esse cache é volátil (perde ao fechar o app), mas já melhora a experiência. Se você quiser cache persistente, uma abordagem comum é salvar em banco local e definir uma política de expiração; como persistência já foi tratada em outro capítulo, aqui o foco fica no cache HTTP e em memória.
Checklist de implementação (passo a passo)
- Adicionar dependências de Retrofit, OkHttp, logging interceptor e Moshi/Gson.
- Criar
OkHttpClientcom timeouts. - Adicionar interceptors: headers (token/opcionais) e logs (apenas debug).
- Configurar cache do OkHttp (tamanho e diretório).
- Criar
RetrofitcombaseUrle converter. - Definir interface
GitHubApicom endpoints e parâmetros. - Modelar DTOs conforme JSON e mapear para modelos de domínio.
- Implementar repositório com coroutines, tratamento de erro e paginação simples.
- Implementar fluxo de lista (search + paginação) e detalhe (getDetail) consumindo a API pública.
- Adicionar cache em memória para detalhe (e/ou cache HTTP para respostas quando aplicável).