Flutter应用中带进度回调的大体积PDF离线下载方案问询
当然有靠谱的实现方案!在Flutter里处理大体积PDF的异步下载+进度回调完全没问题,既能避免阻塞UI,还能轻松对接进度条组件。我给你分享一个实战中常用的实现思路和代码:
核心实现逻辑
关键是用流式下载(而非一次性加载整个文件到内存),分块接收服务器返回的数据,同时实时计算已下载字节数与总字节数的比例,通过回调传递给UI层更新进度。我们可以借助http包简化网络请求,配合dart:io处理文件写入,用path_provider获取本地合法存储路径。
具体实现步骤
1. 添加依赖
在pubspec.yaml里引入需要的包:
dependencies: flutter: sdk: flutter http: ^1.1.0 path_provider: ^2.1.1
2. 封装下载工具函数
这个函数会处理网络请求、流式写入文件,并通过回调返回进度:
import 'dart:io'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; typedef DownloadProgressCallback = void Function(int receivedBytes, int totalBytes); Future<String> downloadLargeFile( String url, String fileName, DownloadProgressCallback onProgress, ) async { try { // 获取应用私有存储目录(无需权限) final directory = await getApplicationDocumentsDirectory(); final savePath = '${directory.path}/$fileName'; final file = File(savePath); // 发起请求获取流式响应 final response = await http.get(Uri.parse(url)).timeout(const Duration(minutes: 10)); final totalBytes = response.contentLength ?? 0; int receivedBytes = 0; // 打开文件写入流 final sink = file.openWrite(); // 分块写入并更新进度 for (final chunk in response.bodyBytes) { sink.add([chunk]); receivedBytes += chunk.length; onProgress(receivedBytes, totalBytes); } await sink.close(); return savePath; // 返回下载后的文件路径 } catch (e) { throw Exception('下载失败: ${e.toString()}'); } }
3. UI层集成进度条
用ValueNotifier来管理进度状态,配合LinearProgressIndicator实现实时更新:
import 'package:flutter/material.dart'; class DownloadScreen extends StatefulWidget { const DownloadScreen({super.key}); @override State<DownloadScreen> createState() => _DownloadScreenState(); } class _DownloadScreenState extends State<DownloadScreen> { final ValueNotifier<double> _progressNotifier = ValueNotifier(0.0); bool _isDownloading = false; String? _downloadedFilePath; Future<void> _startDownload() async { setState(() => _isDownloading = true); try { final pdfUrl = 'https://example.com/large-file.pdf'; // 替换为你的PDF地址 final filePath = await downloadLargeFile( pdfUrl, 'my_large_file.pdf', (received, total) { if (total > 0) { _progressNotifier.value = received / total; } }, ); setState(() => _downloadedFilePath = filePath); } catch (e) { // 处理下载错误,比如弹窗提示 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('下载出错: $e')), ); } } finally { setState(() => _isDownloading = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('大文件下载示例')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_isDownloading) ValueListenableBuilder<double>( valueListenable: _progressNotifier, builder: (context, progress, child) { return Column( children: [ LinearProgressIndicator(value: progress), const SizedBox(height: 16), Text('已完成: ${(progress * 100).toStringAsFixed(1)}%'), ], ); }, ), if (!_isDownloading && _downloadedFilePath != null) Text('文件已保存到: $_downloadedFilePath'), const SizedBox(height: 32), ElevatedButton( onPressed: _isDownloading ? null : _startDownload, child: Text(_isDownloading ? '下载中...' : '开始下载PDF'), ), ], ), ), ); } @override void dispose() { _progressNotifier.dispose(); super.dispose(); } }
额外注意事项
- 权限问题:如果是存储到应用私有目录(上面用的
getApplicationDocumentsDirectory),Android和iOS都不需要额外权限;如果要存到公共目录,需要分别配置对应权限。 - 断点续传:如果需要支持断点,可以在下载前检查本地已下载的字节数,请求时添加
Range: bytes=$received-头,继续下载剩余部分。 - 后台下载:如果需要应用退到后台仍能继续下载,Android可以用
workmanager,iOS可以用background_fetch,不过这是进阶需求,普通场景上面的方案足够。
内容的提问来源于stack exchange,提问作者Kevin Walter
相关产品推荐
相关产品推荐

