Flutter中如何实现Power BI报表的PDF下载?
解决方案:Power BI报表PDF导出下载问题修复
问题核心分析
你的代码通过拦截.pdf结尾的导航请求触发下载,但Power BI导出PDF的常规流程并非直接跳转至PDF URL,而是通过JavaScript生成Blob对象后触发前端下载,这种场景下onNavigationRequest无法捕获请求。另外,直接用dio下载时,会缺失WebView中的Power BI认证Cookie,导致下载内容异常或权限验证失败。
步骤1:替换导航拦截为WebView下载处理器
使用WebView原生的setDownloadHandlerAPI替代导航拦截,它能直接捕获WebView内所有下载请求(包括Blob生成的下载):
修改_initializeWebView方法:
void _initializeWebView() { controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setDownloadHandler((DownloadRequest request) async { // 捕获所有下载请求并触发下载逻辑 await _downloadPdf(request.url); return true; // 返回true表示已接管下载处理 }) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) {}, onPageStarted: (String url) {}, onPageFinished: (String url) {}, onWebResourceError: (WebResourceError error) { print('Error loading page: ${error.description}'); }, // 移除原有的.pdf后缀拦截逻辑 ), ) ..loadRequest(Uri.parse(reportUrl)); }
步骤2:携带Power BI认证Cookie下载
直接用dio下载时,WebView的会话Cookie不会自动携带,导致Power BI返回权限错误。需先从WebView中获取Cookie,再添加到dio请求头:
修改_downloadPdf方法:
Future<void> _downloadPdf(String url) async { // 适配Android 13+存储权限 Permission storagePermission; if (Platform.isAndroid && await AndroidInfo.version >= 13) { storagePermission = Permission.manageExternalStorage; } else { storagePermission = Permission.storage; } if (await storagePermission.request().isGranted) { var dio = Dio(); try { // 从WebView获取当前会话Cookie String? cookie = await controller.runJavaScriptReturningResult( "document.cookie" ) as String?; // 添加Cookie到请求头 if (cookie != null && cookie.isNotEmpty) { dio.options.headers["Cookie"] = cookie; } // 获取公共下载目录,方便用户查找文件 Directory? downloadDir; if (Platform.isAndroid) { downloadDir = await getExternalStorageDirectory(); // Android 13+ 可引入path_provider_extras包使用getDownloadsDirectory() } else if (Platform.isIOS) { downloadDir = await getApplicationDocumentsDirectory(); } if (downloadDir == null) { showToastMessage('无法获取存储目录'); return; } // 生成唯一文件名,避免重复 String fileName = url.split('/').last; if (!fileName.endsWith('.pdf')) { fileName = 'Sales_Report_${DateTime.now().millisecondsSinceEpoch}.pdf'; } String filePath = '${downloadDir.path}/$fileName'; await dio.download(url, filePath); showToastMessage('PDF已保存至:$filePath'); } catch (e) { print('下载错误:$e'); showToastMessage('下载失败,请重试'); } } else { showToastMessage('存储权限被拒绝'); } }
步骤3:适配Android 13+存储权限配置
Android 13及以上版本废弃了WRITE_EXTERNAL_STORAGE权限,如需保存到公共目录,需在AndroidManifest.xml中添加对应权限:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
步骤4:处理Power BI Blob导出场景
如果Power BI通过Blob生成PDF(无直接PDF URL),需注入JavaScript拦截Blob下载事件,将Blob转为Base64后传递给Flutter原生处理:
在onPageFinished中注入JS并注册回调:
onPageFinished: (String url) { // 注入JS拦截Blob类型的PDF下载 controller.runJavaScript(''' // 重写XMLHttpRequest监听Blob响应 const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function() { this.addEventListener('load', function() { if (this.responseType === 'blob' && this.getResponseHeader('Content-Type') === 'application/pdf') { const reader = new FileReader(); reader.onload = function() { window.flutterPdfHandler.postMessage(reader.result); }; reader.readAsDataURL(this.response); } }); originalXHRSend.apply(this, arguments); }; // 重写fetch监听Blob响应 const originalFetch = fetch; window.fetch = async function() { const response = await originalFetch.apply(this, arguments); if (response.headers.get('Content-Type') === 'application/pdf') { const blob = await response.blob(); const reader = new FileReader(); reader.onload = function() { window.flutterPdfHandler.postMessage(reader.result); }; reader.readAsDataURL(blob); } return response; }; '''); // 注册Flutter回调接收Base64格式PDF controller.addJavaScriptChannel( name: 'flutterPdfHandler', onMessageReceived: (JavaScriptMessage message) async { if (message.message.startsWith('data:application/pdf;base64,')) { String base64Pdf = message.message.split(',')[1]; await _saveBase64Pdf(base64Pdf); } }, ); },
添加Base64 PDF保存方法:
Future<void> _saveBase64Pdf(String base64Pdf) async { Permission storagePermission; if (Platform.isAndroid && await AndroidInfo.version >= 13) { storagePermission = Permission.manageExternalStorage; } else { storagePermission = Permission.storage; } if (await storagePermission.request().isGranted) { try { Directory? downloadDir; if (Platform.isAndroid) { downloadDir = await getExternalStorageDirectory(); } else if (Platform.isIOS) { downloadDir = await getApplicationDocumentsDirectory(); } if (downloadDir == null) { showToastMessage('无法获取存储目录'); return; } String fileName = 'Sales_Report_${DateTime.now().millisecondsSinceEpoch}.pdf'; String filePath = '${downloadDir.path}/$fileName'; // 将Base64解码为文件 Uint8List bytes = base64Decode(base64Pdf); await File(filePath).writeAsBytes(bytes); showToastMessage('PDF已保存至:$filePath'); } catch (e) { print('保存错误:$e'); showToastMessage('保存失败,请重试'); } } else { showToastMessage('存储权限被拒绝'); } }
验证顺序
- 优先测试
setDownloadHandler逻辑,这是覆盖大多数场景的解决方案。 - 若下载失败,检查控制台打印的Cookie是否有效,确认权限验证是否通过。
- 若Power BI为Blob导出方式,启用JS拦截逻辑处理Base64内容。
内容的提问来源于stack exchange,提问作者Jay Patel
相关产品推荐
相关产品推荐

