---
title: "Coil com Jetpack Compose: carregar imagens em Kotlin no Android 2026"
url: "https://kotlin.dev.br/blog/coil-jetpack-compose-kotlin-2026/"
markdown_url: "https://kotlin.dev.br/blog/coil-jetpack-compose-kotlin-2026.MD"
description: "Aprenda Coil com Jetpack Compose em Kotlin: AsyncImage, cache, placeholders, transformações, erros, Ktor/OkHttp, testes e boas práticas para apps Android."
date: "2026-07-27"
author: "Karina Melo"
---

# Coil com Jetpack Compose: carregar imagens em Kotlin no Android 2026

Aprenda Coil com Jetpack Compose em Kotlin: AsyncImage, cache, placeholders, transformações, erros, Ktor/OkHttp, testes e boas práticas para apps Android.


**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](/tutoriais/jetpack-compose-basico/), [Paging 3](/blog/paging-3-kotlin-compose-2026/), [offline-first](/blog/android-offline-first-kotlin-2026/) e [WorkManager](/blog/workmanager-kotlin-android-2026/), 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:

1. **coroutines** para trabalho assíncrono cancelável;
2. **Compose** com composables oficiais;
3. **cache** em memória e em disco;
4. **requests** configuráveis (headers, transformações, tamanho, política de cache);
5. **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](/blog/camerax-compose-kotlin-2026/). Para listas enormes, combine Coil com [Paging 3](/blog/paging-3-kotlin-compose-2026/).

## 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) — `Icon` e `Image` do 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:

```kotlin
// 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](/blog/gradle-version-catalog-kotlin-2026/) 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:

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

1. **`contentDescription`**: acessibilidade não é opcional. Avatares decorativos podem usar `null`; fotos com significado semântico precisam de descrição. Veja também [acessibilidade no Compose](/blog/acessibilidade-android-compose-kotlin-2026/).
2. **`contentScale`**: `Crop`, `Fit`, `FillWidth` e `Inside` mudam o resultado visual e o desperdício de pixels decodificados.
3. **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`:

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

```kotlin
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](/blog/hilt-android-kotlin-modulos-scopes-testes-2026/) ou [Koin](/blog/koin-vs-hilt-kotlin-android-2026/)):

```kotlin
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](/blog/ktor-client-resiliente-timeout-retry-circuit-breaker-2026/) 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:

1. **defina tamanho de destino** no composable (`Modifier.size`, aspect ratio fixo ou constraints claras);
2. **não use a URL full-size** se a API oferecer thumbnail; peça a variante certa;
3. **preserve keys estáveis** no `LazyColumn` (`key = { item.id }`) para o Compose reutilizar nós;
4. **evite recarregar** ao rolar para cima e para baixo — cache de memória + disco existe para isso;
5. **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:

```kotlin
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](/blog/paging-3-kotlin-compose-2026/) 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=etag` ou 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](/blog/android-offline-first-kotlin-2026/) e [DataStore](/tutoriais/datastore-preferences-kotlin/) 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:

```kotlin
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](/blog/ktor-authentication-jwt-kotlin-2026/) 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:

1. reserve espaço fixo (aspect ratio) para a imagem antes do load;
2. use placeholder de baixa fidelidade (cor sólida, blur hash ou thumb minúsculo) em vez de spinner em todo card;
3. mantenha o mesmo `contentScale` entre placeholder e imagem final;
4. evite trocar a URL do mesmo item sem necessidade — isso invalida cache visualmente;
5. 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](/blog/animacoes-jetpack-compose-kotlin-2026/) 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 `ImageLoader` por processo;
- [ ] cache de memória e disco habilitados com limites conscientes;
- [ ] `contentDescription` correto (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](/blog/baseline-profiles-macrobenchmark-android-kotlin-2026/);
- [ ] 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:

1. **fake ImageLoader** ou interceptor que devolve drawable/byte local;
2. **fixtures** em `androidTest/assets` ou `test/resources`;
3. **screenshot tests** para regressão visual de cards com imagem — veja [testes de screenshot no Compose](/blog/testes-screenshot-compose-kotlin-2026/);
4. 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](/blog/lgpd-android-kotlin-2026/).

## 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](/blog/sqldelight-kotlin-multiplatform-android-ios-2026/) e [Room KMP](/blog/room-kotlin-multiplatform-2026/).

### 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](/tutoriais/jetpack-compose-basico/), ao [Paging 3](/blog/paging-3-kotlin-compose-2026/), ao [offline-first](/blog/android-offline-first-kotlin-2026/), às [permissões no Android](/blog/permissoes-android-kotlin-2026/) e ao [roadmap de dev Android](/carreira/roadmap-dev-android/). Se o seu momento é carreira e primeira vaga, o [roadmap de Kotlin júnior](/carreira/kotlin-junior-roadmap-primeira-vaga-2026/) e as [vagas Kotlin](/vagas/) ajudam a transformar prática de UI em portfólio demonstrável.
