En la web abierta, un enlace <a download> o un endpoint de PDF/CSV guarda un archivo en el dispositivo. Dentro de un WebView, ese mismo toque es un no-op silencioso. Android solo expone las descargas mediante un DownloadListener del host, e iOS WKWebView no trae gestor de descargas. Un runtime propio del host hace la ponte de la descarga: una API que guarda un archivo real en cada motor.

Android: las descargas necesitan un listener del host

La documentación de Android y la comunidad coinciden: un WebView estándar no descarga nada por sí solo. Cuando el usuario toca un enlace de descarga, el motor entrega los parámetros —url, userAgent, contentDisposition, mimeType, contentLength— al DownloadListener que registras con setDownloadListener, o no pasa nada. Sin ese listener, un botón de "Descargar factura PDF" que funciona en Chrome no hace absolutamente nada dentro del WebView de la app. Conectarlo implica poseer la instancia, parsear el nombre de archivo desde Content-Disposition, solicitar WRITE_EXTERNAL_STORAGE (o delegar a DownloadManager) y mostrar progreso. Las descargas blob y <a download> son lo mismo: la página no puede escribir el archivo; solo el host puede.

iOS: WKWebView no tiene gestor de descargas

El WebView de Apple no tiene una canalización de descarga integrada. Los análisis de las limitaciones de WKWebView lo dicen claro: "No hay gestor de descargas, así que las descargas fallan o no hacen nada en silencio". Antes de iOS 15 la única vía era interceptar la petición y reimplementar el guardado. iOS 15 añadió WKDownloadDelegate, pero el host debe implementarlo: el atributo <a download> solo señala intención, y la ejecución vive en la app. El comportamiento por defecto es peor que un no-op: el archivo a menudo se abre dentro del marco web y rompe el historial de navegación, o el toque se traga sin feedback.

Qué se rompe cuando la descarga muere

  • Facturas y estados nunca llegan. Un botón "Descargar PDF" que funciona en Safari no hace nada en el WebView.
  • Exportaciones CSV e informes desaparecen. Los paneles SaaS y financieros que exportan datos pierden la única salida.
  • Actualizaciones y APKs se estancan. Los flujos de actualización que empujan un APK o un paquete fallan dentro del navegador integrado.
  • Los navegadores integrados empeoran. WeChat, Douyin y similares rara vez exponen una superficie de descarga, así que el fallo se oculta en el QA de escritorio.

La solución del runtime

Un runtime propio del host hace la ponte de la descarga. La página llama a una API uniforme —runtime.download(url, options) o runtime.saveFile(blob)— y el host guarda con DownloadManager en Android y WKDownloadDelegate en iOS. Nombre, destino, progreso y permiso de almacenamiento se manejan de forma nativa y consistente. Una integración, cada motor, un archivo guardado.

DimensiónWebViewRuntime propio del host
Disparador<a download> es no-opruntime.download() uniforme
Ruta AndroidEl host debe cablear DownloadListenerDownloadManager nativo
Ruta iOSSin gestor de descargasWKDownloadDelegate nativo
Guardar blobNo desde la páginaUna API
PermisosManual por appPonte nativa
ConsistenciaPor motorUniforme

Una app nativa guarda un archivo en cada dispositivo. Un WebView estándar apenas en ninguno. Un runtime da a la página una acción de guardar — y un archivo real al final.

Fuentes