Na web aberta, um link <a download> ou um endpoint de PDF/CSV salva um arquivo no dispositivo. Dentro de um WebView, esse mesmo toque é um no-op silencioso. O Android só expõe downloads por meio de um DownloadListener do host, e o iOS WKWebView não traz gerenciador de downloads. Um runtime próprio do host faz a ponte do download: uma API que salva um arquivo real em cada motor.
Android: os downloads precisam de um listener do host
A documentação do Android e a comunidade concordam: um WebView padrão não baixa nada sozinho. Quando o usuário toca um link de download, o motor entrega os parâmetros —url, userAgent, contentDisposition, mimeType, contentLength— ao DownloadListener que você registra com setDownloadListener, ou nada acontece. Sem esse listener, um botão "Baixar fatura PDF" que funciona no Chrome não faz absolutamente nada dentro do WebView do app. Conectá-lo significa possuir a instância, extrair o nome do arquivo de Content-Disposition, solicitar WRITE_EXTERNAL_STORAGE (ou delegar ao DownloadManager) e mostrar progresso. Downloads blob e <a download> são a mesma história: a página não pode gravar o arquivo; só o host pode.
iOS: WKWebView não tem gerenciador de downloads
O WebView da Apple não tem um pipeline de download integrado. Textos sobre as limitações do WKWebView dizem claro: "Não há gerenciador de downloads, então os downloads falham ou não fazem nada em silêncio". Antes do iOS 15, a única via era interceptar a requisição e reimplementar o salvamento. O iOS 15 adicionou WKDownloadDelegate, mas o host precisa implementá-lo: o atributo <a download> apenas sinaliza intenção, e a execução vive no app. O comportamento padrão é pior que um no-op: o arquivo frequentemente abre dentro do frame web e quebra o histórico de navegação, ou o toque é engolido sem feedback.
O que quebra quando o download morre
- Faturas e extratos não chegam. Um botão "Baixar PDF" que funciona no Safari não faz nada no WebView.
- Exportações CSV e relatórios somem. Painéis SaaS e financeiros que exportam dados perdem a única saída.
- Atualizações e APKs travam. Fluxos de atualização que empurram um APK ou pacote falham dentro do navegador integrado.
- Navegadores integrados pioram. WeChat, Douyin e similares raramente expõem uma superfície de download, então a falha se esconde no QA de desktop.
A solução do runtime
Um runtime próprio do host faz a ponte do download. A página chama uma API uniforme —runtime.download(url, options) ou runtime.saveFile(blob)— e o host salva com DownloadManager no Android e WKDownloadDelegate no iOS. Nome, destino, progresso e permissão de armazenamento são tratados de forma nativa e consistente. Uma integração, cada motor, um arquivo salvo.
| Dimensão | WebView | Runtime próprio do host |
|---|---|---|
| Disparador | <a download> é no-op | runtime.download() uniforme |
| Caminho Android | Host liga DownloadListener | DownloadManager nativo |
| Caminho iOS | Sem gerenciador de downloads | WKDownloadDelegate nativo |
| Salvar blob | Não a partir da página | Uma API |
| Permissões | Manual por app | Ponte nativa |
| Consistência | Por motor | Uniforme |
Um app nativo salva um arquivo em cada dispositivo. Um WebView padrão quase em nenhum. Um runtime dá à página uma ação de salvar — e um arquivo real no fim.
Fontes
- dev.to — Como funciona o listener de download do WebView no Android — sem
setDownloadListener, tocar um link de download não faz nada; o listener entregaurl/mimeType/filenameao host. - DevGex — Habilitando o download de arquivos no Android WebView —
setDownloadListenermaisDownloadManager; permissões de armazenamento necessárias. - gotoapp.store — Limitações do WKWebView — "Não há gerenciador de downloads, então os downloads falham ou não fazem nada em silêncio".
- Medium — Baixando arquivos de um WKWebView desde o iOS 15 —
WKDownloadDelegate;<a download>sinaliza intenção mas a execução ainda precisa do host;canShowMimeType. - GitHub MAUI — WebView no iOS não consegue baixar arquivo — o WebView iOS por padrão abre o arquivo na página ou quebra a navegação; é preciso adicionar manipuladores
WKDownloadDelegate.