Resposta rápida: use Coil com Jetpack Compose quando o app Android em Kotlin precisa carregar imagens de rede, disco ou memória de forma assíncrona, com cache, placeholder e tratamento de erro sem bloquear a UI. O ponto de entrada mais comum é AsyncImage (ou SubcomposeAsyncImage quando o layout precisa reagir ao estado de carregamento). Configure um ImageLoader único por processo, defina tamanho de destino, use HTTPS, trate falhas e evite recriar o loader a cada recomposição. Coil é a escolha padrão de muitos times Kotlin porque nasceu com coroutines e se integra naturalmente ao Compose.
Carregar imagem parece detalhe de UI até o feed começar a travar, a memória subir, o placeholder piscar sem parar ou o mesmo avatar ser baixado dezenas de vezes. Em apps reais — e-commerce, delivery, redes sociais, fintech, marketplaces e painéis internos — imagens dominam o payload visual e boa parte do consumo de rede em 4G brasileiro. Coil resolve isso com uma API enxuta: request, transformações, cache em memória e disco, cancelamento automático e integração com Compose.
Este guia mostra quando usar Coil, como configurar dependências, como montar AsyncImage e SubcomposeAsyncImage, como customizar o ImageLoader, como lidar com cache, headers, autenticação, listas grandes, erros comuns e testes. Se você já trabalha com Jetpack Compose básico, Paging 3, offline-first e WorkManager, Coil completa a trilha de mídia e performance de tela.
O que é Coil?
Coil significa Coroutine Image Loader. É uma biblioteca de carregamento de imagens escrita em Kotlin, com foco em:
- coroutines para trabalho assíncrono cancelável;
- Compose com composables oficiais;
- cache em memória e em disco;
- requests configuráveis (headers, transformações, tamanho, política de cache);
- extensibilidade via interceptors, fetchers e decoders.
Em projetos Android modernos, Coil costuma substituir Picasso e, em muitos times Kotlin, também Glide. A API fica mais próxima do restante do código Kotlin: builders com defaults sensatos, menos boilerplate e boa integração com Ktor/OkHttp quando o app já padroniza a camada de rede.
Coil não é um framework de galeria, editor de fotos ou player de vídeo. Ele resolve o problema de baixar, decodificar, redimensionar, cachear e exibir imagens de forma eficiente. Para câmera e captura, use CameraX com Compose. Para listas enormes, combine Coil com Paging 3.
Quando usar Coil?
Use Coil quando a tela precisa de:
- avatares, capas, thumbnails e banners remotos;
- imagens de catálogo com placeholder e fallback;
- listas com dezenas ou centenas de imagens;
- cache local para reduzir tráfego e latência;
- transformação simples (crop circular, blur leve, arredondamento);
- carregamento a partir de URL,
Uri, arquivo, recurso ou modelo customizado.
Pense duas vezes antes de usar Coil quando:
- a tela só mostra ícones vetoriais locais (
ImageVector/ SVG estático) —IconeImagedo Compose bastam; - você precisa de edição avançada, filtros profissionais ou pipeline de GPU customizado;
- o arquivo é um vídeo, PDF multipágina ou documento que exige viewer especializado.
Dependências no Gradle Kotlin DSL
Em um app Android com Compose, o setup mínimo costuma ser:
// build.gradle.kts (módulo app)
dependencies {
implementation("io.coil-kt.coil3:coil-compose:<versao-atual>")
implementation("io.coil-kt.coil3:coil-network-okhttp:<versao-atual>")
// ou, se o app já usa Ktor no client:
// implementation("io.coil-kt.coil3:coil-network-ktor3:<versao-atual>")
}
O placeholder de versão evita transformar o artigo em fonte de pin desatualizado. No projeto real, fixe a versão no version catalog do Gradle e atualize por pull request. Se o app ainda usa Coil 2, a API de AsyncImage é parecida, mas os artefatos e alguns defaults de rede mudaram na linha 3 — leia o changelog antes de migrar.
Projetos multiplataforma ou com formatos adicionais podem precisar de artefatos extras (SVG, GIF animado, vídeo frame). Adicione apenas o que a UI realmente consome: cada decoder aumenta superfície de ataque, tamanho do APK e complexidade de teste.
AsyncImage: o caminho mais curto
O uso mais comum em Compose é direto:
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@Composable
fun AvatarUsuario(
url: String?,
nome: String,
modifier: Modifier = Modifier
) {
AsyncImage(
model = url,
contentDescription = "Foto de $nome",
contentScale = ContentScale.Crop,
modifier = modifier.size(48.dp)
)
}
Três detalhes importam mais do que parece:
contentDescription: acessibilidade não é opcional. Avatares decorativos podem usarnull; fotos com significado semântico precisam de descrição. Veja também acessibilidade no Compose.contentScale:Crop,Fit,FillWidtheInsidemudam o resultado visual e o desperdício de pixels decodificados.- tamanho no
Modifier: ajuda o Coil a decodificar em resolução adequada em vez de materializar a imagem original inteira na memória.
ImageRequest: controle fino sem reinventar o loader
Quando você precisa de placeholder, erro, crossfade ou headers, construa um ImageRequest:
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
@Composable
fun CapaProduto(url: String, modifier: Modifier = Modifier) {
val context = LocalContext.current
AsyncImage(
model = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.memoryCacheKey(url)
.diskCacheKey(url)
.build(),
contentDescription = "Capa do produto",
modifier = modifier
)
}
Use ImageRequest quando a tela precisa de políticas diferentes da global. Não construa um ImageLoader novo a cada item da lista — isso é um dos erros mais caros de performance.
SubcomposeAsyncImage: UI que reage ao estado
AsyncImage cobre a maioria dos casos. Use SubcomposeAsyncImage quando placeholder, loading e erro exigem composables diferentes — skeleton, botão de tentar de novo, badge de offline:
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
@Composable
fun BannerComEstado(
url: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
SubcomposeAsyncImage(
model = url,
contentDescription = "Banner promocional",
modifier = modifier
) {
when (painter.state) {
is AsyncImagePainter.State.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is AsyncImagePainter.State.Error -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TextButton(onClick = onRetry) {
Text("Não foi possível carregar. Tentar de novo")
}
}
}
else -> SubcomposeAsyncImageContent()
}
}
}
Esse padrão combina bem com feeds offline-first: a UI mostra skeleton enquanto a rede responde e um fallback claro quando o cache e a origem falham.
Configure um ImageLoader único
O ImageLoader é o motor: cache, rede, decoders e políticas. Em apps sérios, configure uma instância por processo, tipicamente no Application ou via DI (Hilt ou Koin):
import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import okhttp3.OkHttpClient
import okio.Path.Companion.toOkioPath
import java.util.concurrent.TimeUnit
fun criarImageLoader(context: Context, okHttp: OkHttpClient): ImageLoader {
val appContext = context.applicationContext
return ImageLoader.Builder(appContext)
.components {
add(OkHttpNetworkFetcherFactory(okHttp))
}
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(appContext, 0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(appContext.cacheDir.resolve("coil_images").toOkioPath())
.maxSizeBytes(128L * 1024L * 1024L)
.build()
}
.crossfade(true)
.build()
}
fun okHttpPadrao(): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.build()
Benefícios de um loader único:
- reutiliza conexões HTTP e pool de threads;
- compartilha cache entre telas e módulos de feature;
- permite interceptors de autenticação, logging redacted e métricas;
- evita thrashing de memória quando o usuário navega rápido entre listas.
Se o app já usa Ktor Client para API, alinhar Coil ao mesmo stack de rede reduz divergência de timeouts, certificados e interceptors. O guia de Ktor Client resiliente mostra timeout, retry e circuit breaker do lado de API — os mesmos princípios de resiliência valem para mídia.
Listas, LazyColumn e Paging
Imagens em lista são o cenário que mais pune má configuração. Algumas regras práticas:
- defina tamanho de destino no composable (
Modifier.size, aspect ratio fixo ou constraints claras); - não use a URL full-size se a API oferecer thumbnail; peça a variante certa;
- preserve keys estáveis no
LazyColumn(key = { item.id }) para o Compose reutilizar nós; - evite recarregar ao rolar para cima e para baixo — cache de memória + disco existe para isso;
- cancele o trabalho ao sair da tela — Coil e Compose já cooperam quando o request está amarrado ao ciclo de vida do composable.
Exemplo em lista:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
data class ItemCatalogo(val id: String, val titulo: String, val imagemUrl: String)
@Composable
fun ListaCatalogo(itens: List<ItemCatalogo>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = itens,
key = { it.id }
) { item ->
AsyncImage(
model = item.imagemUrl,
contentDescription = item.titulo,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
)
}
}
}
Quando a lista vem paginada do backend e do Room, o guia de Paging 3 com Compose mostra como combinar LazyPagingItems com carregamento incremental. Coil entra em cada célula; Paging decide quando a célula existe.
Cache: memória, disco e política
Coil trabalha com duas camadas principais:
- memory cache: bitmap (ou imagem decodificada) pronto para reuso imediato;
- disk cache: bytes da resposta para sobreviver a process death e reduzir rede.
Boas práticas:
- deixe o Coil calcular chaves a partir de URL + tamanho + transformações, a menos que você tenha um motivo forte para sobrescrever;
- não desligue cache “para forçar imagem fresca” em produção sem política; prefira cache-busting controlado (
?v=etagou header) quando o backend versiona mídia; - dimensione o disk cache conforme o perfil do app: feed de fotos precisa de mais espaço do que um app bancário com poucos avatares;
- em dispositivos low-end, reduza o percentual de memory cache e prefira thumbnails.
Offline-first de verdade não é só Room: imagens também precisam de estratégia. Se o usuário abriu o produto ontem e abriu de novo no metrô sem sinal, o disk cache do Coil é o que salva a percepção de qualidade. Combine com Android offline-first e DataStore para flags de “última sincronização” e modo degradado.
Headers, autenticação e URLs assinadas
Nem toda imagem é pública. Buckets privados, CDNs com token e avatares atrás de API autenticada pedem cuidado:
import coil3.request.ImageRequest
fun requestAutenticada(
context: android.content.Context,
url: String,
token: String
): ImageRequest =
ImageRequest.Builder(context)
.data(url)
.addHeader("Authorization", "Bearer $token")
.build()
Regras de segurança:
- não coloque tokens de longa duração em logs de interceptor de imagem;
- prefira URLs assinadas de curta duração emitidas pelo backend quando possível;
- se o token muda com frequência, a chave de cache deve considerar isso ou a URL assinada já será única;
- nunca embuta segredos de CDN no APK.
Para APIs que já usam JWT, o mesmo raciocínio de expiração curta do guia de Ktor Authentication com JWT se aplica a mídia protegida.
Placeholder, erro e estabilidade visual
Piscar placeholder a cada recomposição destrói a sensação de app polido. Minimize isso:
- reserve espaço fixo (aspect ratio) para a imagem antes do load;
- use placeholder de baixa fidelidade (cor sólida, blur hash ou thumb minúsculo) em vez de spinner em todo card;
- mantenha o mesmo
contentScaleentre placeholder e imagem final; - evite trocar a URL do mesmo item sem necessidade — isso invalida cache visualmente;
- em temas claro/escuro, escolha placeholders que não estouram contraste.
Animações de entrada ajudam, mas o excesso atrasa a percepção de lista pronta. O guia de animações no Jetpack Compose mostra quando animar com propósito.
Transformações e formas
Casos comuns de UI:
- avatar circular;
- capa com cantos arredondados;
- center-crop em banner;
- downsampling agressivo para notificação ou widget.
No Compose, boa parte do “recorte visual” pode ser feita com Modifier.clip e ContentScale, sem transformação de bitmap. Prefira o caminho de UI quando o objetivo é só apresentação: é mais barato e reutiliza o mesmo bitmap em layouts diferentes.
Use transformação no pipeline do Coil quando o bitmap em cache precisa nascer já processado (por exemplo, gerar thumb padronizado para várias telas). Transformar cedo demais multiplica variantes no cache.
Performance e memória
Checklist de performance com Coil + Compose:
- tamanho de destino definido na UI;
- variante de imagem adequada (thumb vs full);
- um
ImageLoaderpor processo; - cache de memória e disco habilitados com limites conscientes;
-
contentDescriptioncorreto (a11y sem custo de runtime relevante); - listas com keys estáveis;
- sem conversão desnecessária para bitmap em thread principal;
- sem logs verbosos de URL com query string sensível;
- Baseline Profile / startup quando a home é imagem-pesada — veja Baseline Profiles e Macrobenchmark;
- medição real em device intermediário, não só no emulador top de linha.
Se a home carrega um carrossel + grid + avatares, o custo de decode compete com composição e rede. Meça jank e memória antes de “otimizar no escuro”.
Coil, Glide e Picasso: como decidir?
| Critério | Coil | Glide | Picasso |
|---|---|---|---|
| Idioma e API | Kotlin-first, coroutines | Maduro, API Java-friendly | Simples, menos features modernas |
| Compose | Suporte oficial forte | Possível via integrações | Possível, menos ergonômico |
| Cache e transformações | Sólido para a maioria dos apps | Muito completo | Básico a intermediário |
| Ecossistema Kotlin | Excelente | Bom | Limitado em apps novos |
| Curva em app novo Kotlin | Baixa | Média | Baixa, mas teto menor |
Para app Android novo em Kotlin + Compose em 2026, Coil é a escolha padrão na maioria dos times. Migre de Glide apenas com plano: inventário de transformações customizadas, módulos de GIF/SVG e testes visuais das telas críticas. Se o legado Glide está estável e sem dor, a migração não precisa ser heróica — foque em features novas.
Testes
Testar UI com rede real é flaky. Estratégias melhores:
- fake ImageLoader ou interceptor que devolve drawable/byte local;
- fixtures em
androidTest/assetsoutest/resources; - screenshot tests para regressão visual de cards com imagem — veja testes de screenshot no Compose;
- testes de ViewModel/repository sem Coil, validando apenas a URL/modelo exposto à UI.
Exemplo de mentalidade: o teste do card não precisa baixar o logo da empresa na internet; precisa garantir que, dado um modelo, o composable monta o request certo e os estados de loading/erro aparecem.
Erros comuns
Recriar ImageLoader em todo composable
Causa pressão de memória, perde cache compartilhado e multiplica threads. Injete ou obtenha o loader singleton.
Decodificar a imagem original inteira
Sem tamanho de destino, um JPEG de 4000px vira bomba de memória em lista. Defina constraints e use thumbnails.
contentDescription errado ou ausente
Impacta TalkBack e auditorias de acessibilidade. Decoreção pura → null. Conteúdo informativo → texto útil em português claro.
Misturar HTTP claro em produção
Use HTTPS. Cleartext exige configuração explícita e amplia risco. Em redes brasileiras de operadora e Wi-Fi público, middleboxes também se comportam melhor com TLS.
Tratar falha de imagem como falha de app
Uma URL 404 de avatar não deveria derrubar a sessão. Mostre fallback e siga o fluxo.
Cache eterno de URL mutável
Se https://cdn/app/avatar muda o binário sem mudar a URL, usuários veem foto antiga. Versionamento de URL ou política de invalidação resolve.
Trabalho pesado de transformação na main thread
Transformações customizadas mal implementadas travam frames. Prefira APIs do Coil e decode em tamanho correto.
Dependência de rede em teste instrumentado sem controle
Torna CI instável. Isole rede.
Checklist de produção
Antes de liberar uma tela imagem-pesada:
- loader singleton configurado com cache de memória e disco;
- cliente HTTP com timeouts explícitos;
- HTTPS e sem segredos em URL de log;
- placeholders e erros desenhados no design system;
- tamanhos de imagem acordados com backend/CDN;
- listas com keys e células de altura previsível;
- acessibilidade revisada em TalkBack;
- comportamento offline verificado (cache hit / miss);
- métricas de falha de load (taxa de erro de imagem) em observabilidade;
- screenshot ou UI test nas células críticas;
- revisão de LGPD para fotos de usuário — veja LGPD no Android com Kotlin.
Perguntas frequentes
Coil funciona bem com Jetpack Compose?
Sim. A integração oficial com AsyncImage e SubcomposeAsyncImage é o caminho recomendado em apps Kotlin modernos. Você evita adapters legados de ImageView e mantém o carregamento alinhado ao ciclo de recomposição.
Preciso de Glide se já uso Coil?
Na maioria dos apps novos, não. Glide ainda aparece em legados e em pipelines muito específicos de transformação. Avalie migração por custo, não por moda.
AsyncImage substitui Image do Compose?
Não. Image continua ideal para recursos locais estáveis (painterResource, vetores). AsyncImage brilha com fontes assíncronas e cacheáveis (rede, disco, modelos compostos).
Como lidar com imagens autenticadas?
Prefira URLs assinadas de curta duração. Se precisar de header Authorization, configure no ImageRequest ou em interceptor do cliente HTTP compartilhado, sem logar o token.
Coil serve para Kotlin Multiplatform?
Há evolução do ecossistema Coil em direção multiplataforma, mas o caso dominante deste guia é Android + Compose. Em KMP, valide artefatos e backends de imagem por target antes de padronizar. Para persistência multiplataforma, compare também SQLDelight e Room KMP.
Imagens pesadas atrasam a startup do app?
Podem atrasar a home, especialmente com carrosséis grandes. Use placeholders leves, adie conteúdo below-the-fold, prefira thumbs e meça com Macrobenchmark. Startup cold e tempo até primeiro frame útil importam tanto quanto a beleza do banner.
Conclusão
Coil com Jetpack Compose é a combinação mais natural para carregar imagens em apps Android escritos em Kotlin em 2026: API enxuta, coroutines, cache e composables oficiais. O segredo não está em decorar a tela com AsyncImage e sim em tratar imagem como parte da arquitetura — loader único, tamanho de destino, política de cache, fallback offline, acessibilidade e medição.
Comece simples: AsyncImage + loader configurado no Application + placeholders honestos. Depois, evolua para headers autenticados, listas paginadas, screenshot tests e métricas de erro. Evite copiar configurações de apps de rede social para um produto com dez avatares — e evite também ir a produção com URLs full-size em LazyColumn sem cache.
Para seguir a trilha Android, conecte este guia ao tutorial de Jetpack Compose, ao Paging 3, ao offline-first, às permissões no Android e ao roadmap de dev Android. Se o seu momento é carreira e primeira vaga, o roadmap de Kotlin júnior e as vagas Kotlin ajudam a transformar prática de UI em portfólio demonstrável.