You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 03:56:07