Programação Por Equipe DevBras

Capa do artigo: Glide não carrega a imagem: como achar a causa

Glide não carrega a imagem: como achar a causa

Glide preso no placeholder? Faça o Glide mostrar o erro, veja o que o servidor devolve com um script curl e confira cleartext, URL nula e cache da foto.

A foto do produto fica no placeholder para sempre, mas a mesma URL abre normalmente no navegador do computador. Isso não quer dizer que o Glide esteja quebrado. Quase sempre a resposta que chega ao celular é diferente da que o navegador recebe, ou o próprio Android barrou a requisição antes de ela sair. Este roteiro serve para apps com ImageView e RecyclerView usando Glide 5.0.9, a versão atual no Maven Central. Ele vai em três passos: fazer o Glide dizer o erro, ver o que o servidor devolve e checar as regras do Android.

Se o app é em Jetpack Compose, a configuração muda bastante; veja o guia de imagem remota no Compose com Coil 3.

1. Faça o Glide dizer o motivo

Por padrão, uma carga que falha só troca a imagem. O motivo aparece no Logcat ligando a tag do Glide no aparelho, como manda a página de depuração do Glide:

adb shell setprop log.tag.Glide DEBUG

Para registrar a falha no seu próprio log, ou mandar para o serviço de erros do app, use um RequestListener. O GlideException pode ter várias causas raiz, porque o Glide tenta mais de um caminho para carregar cada modelo. O logRootCauses imprime todas:

Glide.with(holder.itemView)
    .load(produto.fotoUrl)
    .placeholder(R.drawable.foto_carregando)
    .error(R.drawable.foto_indisponivel)
    .fallback(R.drawable.foto_sem_cadastro)
    .listener(object : RequestListener<Drawable> {
        override fun onLoadFailed(
            e: GlideException?, model: Any?,
            target: Target<Drawable>, isFirstResource: Boolean
        ): Boolean {
            Log.e("FotoProduto", "Falhou: $model")
            e?.logRootCauses("FotoProduto")
            return false   // deixa o Glide mostrar o error
        }
        override fun onResourceReady(
            resource: Drawable, model: Any, target: Target<Drawable>?,
            dataSource: DataSource, isFirstResource: Boolean
        ): Boolean = false
    })
    .into(holder.foto)

A causa raiz costuma ser uma destas: código HTTP inesperado, falha ao decodificar ou uma mensagem com CLEARTEXT dizendo que a política de rede não permite aquele host. Cada uma leva a um dos passos abaixo.

2. Veja o que o servidor devolve de verdade

O navegador do computador tem os seus cookies, segue o caminho de quem já fez login e não liga para um link vencido aberto há cinco minutos. O app não tem nada disso. Para tirar a dúvida, este script baixa cada URL como um cliente sem sessão e compara três coisas: o status HTTP, o tipo que o servidor declara e o tipo real do arquivo, lido nos primeiros bytes.

#!/usr/bin/env bash
# Uso: ./conferir-imagem.sh URL [URL...]
# Mostra o que o servidor devolve de verdade para cada URL de imagem.
tipo_real() {  # olha os primeiros bytes do arquivo
  local h; h=$(head -c 12 "$1" | od -An -tx1 | tr -d ' \n')
  case $h in
    ffd8ff*) echo image/jpeg ;;
    89504e47*) echo image/png ;;
    47494638*) echo image/gif ;;
    52494646????????57454250*) echo image/webp ;;
    *) echo "desconhecido" ;;
  esac
}
for url in "$@"; do
  corpo=$(mktemp)
  IFS=$'\t' read -r status tipo tamanho < <(curl -s -L -o "$corpo" \
      -w '%{http_code}\t%{content_type}\t%{size_download}\n' "$url")
  real=$(tipo_real "$corpo")
  if [[ $status != 200 ]]; then veredito="FALHA HTTP $status"
  elif [[ $real == desconhecido ]]; then veredito="NAO E IMAGEM"
  elif [[ ${tipo%%;*} != "$real" ]]; then veredito="TIPO TROCADO"
  else veredito="OK"; fi
  printf '%-14s %s | %s | real: %s | %s bytes | %s\n' \
    "$veredito" "$status" "$tipo" "$real" "$tamanho" "$url"
  rm -f "$corpo"
done

Rodando contra um servidor local que imita os casos mais comuns:

OK             200 | image/jpeg | real: image/jpeg | 10629 bytes | /produtos/123.jpg
NAO E IMAGEM   200 | text/html; charset=utf-8 | real: desconhecido | 36 bytes | /produtos/124.jpg
FALHA HTTP 403 403 | application/xml | real: desconhecido | 40 bytes | /cdn/125.jpg?expira=1761000000
OK             200 | image/jpeg | real: image/jpeg | 10629 bytes | /cdn/125.jpg?expira=1893456000
TIPO TROCADO   200 | image/jpeg | real: image/png | 1940 bytes | /produtos/126.jpg
FALHA HTTP 404 404 | text/plain | real: desconhecido | 14 bytes | /produtos/999.jpg
  • 124.jpg, 200 com HTML. O endpoint de imagens exige sessão e, sem ela, responde com a página de login, com status 200. O Glide recebe um HTML e falha na decodificação. Se a imagem é privada de propósito, mande a credencial no pedido (próxima seção). Se não é, a rota de imagens não deveria estar atrás do login.
  • 125.jpg com prazo vencido, 403. URLs assinadas de storage têm validade. O app guarda a lista de produtos de uma hora atrás, o usuário volta à tela e as URLs já expiraram. Peça URLs novas à API ao recarregar a lista, ou use um prazo maior que o tempo de vida do cache da lista.
  • 126.jpg, tipo trocado. O cabeçalho diz JPEG e o arquivo é PNG. O Glide identifica o formato pelos bytes, então esse caso costuma carregar, mas vale corrigir no servidor, porque outros clientes e CDNs confiam no cabeçalho.
  • 999.jpg, 404. O cadastro aponta para um arquivo que não existe. Isso se resolve no back-end, não no app.

Imagem que precisa de token

Quando a foto é privada, o Glide aceita cabeçalhos por meio de GlideUrl:

val comToken = GlideUrl(
    produto.fotoUrl,
    LazyHeaders.Builder()
        .addHeader("Authorization", "Bearer $tokenDeAcesso")
        .build()
)
Glide.with(holder.itemView).load(comToken).into(holder.foto)

Um cuidado: a chave de cache do GlideUrl é a URL, sem os cabeçalhos. Se duas contas usam o mesmo aparelho e o servidor devolve fotos diferentes para a mesma URL conforme o token, a segunda conta pode ver a imagem em cache da primeira. URLs distintas por recurso evitam o problema.

3. Confira o que o Android permite

Se a causa raiz fala em cleartext, a URL é http://. A documentação da Network Security Configuration diz que, a partir do Android 9 (API 28), o tráfego em texto puro vem desativado por padrão. A correção de verdade é servir as imagens por HTTPS. Para o servidor de desenvolvimento no emulador, que o Android enxerga como 10.0.2.2, libere só esse endereço, num arquivo que exista apenas no build de debug:

<!-- app/src/debug/res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">10.0.2.2</domain>
    </domain-config>
</network-security-config>

O arquivo precisa ser referenciado no android:networkSecurityConfig do manifesto de debug. Ligar usesCleartextTraffic no app inteiro de produção resolve o sintoma e abre a porta para todo o resto.

URL nula não é erro de rede

Produto sem foto cadastrada chega com fotoUrl nula. A página de placeholders descreve a ordem: com URL nula, o Glide mostra o fallback; sem fallback, mostra o error; sem os dois, deixa o placeholder. Por isso o exemplo acima define as três imagens. Assim "sem foto" e "falhou" aparecem diferentes para o usuário e para quem atende o suporte.

Também não é preciso testar nulo antes de chamar o Glide no onBindViewHolder. O guia de início pede só que toda view reaproveitada receba uma carga nova ou um clear(). Se uma linha especial põe um drawable fixo na ImageView sem chamar clear(), a carga antiga daquela view pode terminar depois e trocar a imagem da linha errada.

Foto trocada no servidor, foto velha no app

Esse caso não aparece como erro. O Glide monta a chave de cache a partir do modelo, aqui a URL. Se o servidor grava a foto nova no mesmo endereço, o app continua com a do cache. A página de cache recomenda, antes de tudo, que o servidor mude a URL quando o conteúdo muda. Quando não dá, misture uma versão na chave:

Glide.with(holder.itemView)
    .load(produto.fotoUrl)
    .signature(ObjectKey(produto.fotoAtualizadaEm))   // muda quando a foto muda
    .into(holder.foto)

E, para o app baixar menos, gere miniaturas no upload, como em redimensionar imagem no servidor.

Avalie este artigo

Referência: https://zarelli.wordpress.com/2011/06/13/android-exibindo-uma-imagem-da-web-em-um-imageview/