Dio下载管理器:暂停、继续与取消下载
你是否遇到过下载大文件时网络中断导致必须重新开始的问题?是否需要在用户切换应用时暂停下载以节省流量?Dio作为Dart和Flutter生态中强大的HTTP客户端,提供了完整的下载管理功能,支持暂停、继续和取消下载操作。本文将详细介绍如何使用Dio实现专业级的下载管理功能。## Dio下载基础Dio提供了两种主要的下载方式:使用`dio.download()`方法和通过获取字节流手动处理。这...
Dio下载管理器:暂停、继续与取消下载
你是否遇到过下载大文件时网络中断导致必须重新开始的问题?是否需要在用户切换应用时暂停下载以节省流量?Dio作为Dart和Flutter生态中强大的HTTP客户端,提供了完整的下载管理功能,支持暂停、继续和取消下载操作。本文将详细介绍如何使用Dio实现专业级的下载管理功能。
Dio下载基础
Dio提供了两种主要的下载方式:使用dio.download()方法和通过获取字节流手动处理。这两种方式都支持下载进度监听和取消操作。
基础下载实现
最基础的下载实现可以直接使用Dio的download方法,如example_dart/lib/download.dart所示:
void main() async {
final dio = Dio();
dio.interceptors.add(LogInterceptor());
// 确保onReceiveProgress的total参数不为-1
dio.options.headers = {HttpHeaders.acceptEncodingHeader: '*'};
final url = 'https://pub.dev/static/hash-rhob5slb/img/pub-dev-logo.svg';
await download1(dio, url, './example/pub-dev-logo.svg');
}
Future download1(Dio dio, String url, savePath) async {
final cancelToken = CancelToken();
try {
await dio.download(
url,
savePath,
onReceiveProgress: showDownloadProgress,
cancelToken: cancelToken,
);
} catch (e) {
print(e);
}
}
void showDownloadProgress(int received, int total) {
if (total <= 0) return;
print('进度: ${(received / total * 100).toStringAsFixed(0)}%');
}
这段代码实现了基本的文件下载功能,包括进度监听。其中CancelToken对象是实现下载取消功能的关键。
取消下载功能
Dio的取消下载功能通过CancelToken实现,它允许你在任何时候取消一个或多个请求。
单个下载的取消
使用CancelToken可以轻松取消单个下载请求。以下是取消请求的基本实现,来自example_dart/lib/cancel_request.dart:
void main() async {
final dio = Dio();
dio.interceptors.add(LogInterceptor());
// 创建一个CancelToken
final cancelToken = CancelToken();
// 设置500毫秒后取消请求
Timer(const Duration(milliseconds: 500), () {
token.cancel('用户取消了下载');
});
try {
await dio.download(
'https://example.com/large_file.zip',
'./download/large_file.zip',
cancelToken: cancelToken,
onReceiveProgress: showDownloadProgress,
);
} catch (e) {
if (CancelToken.isCancel(e)) {
print('下载已取消: ${e.message}');
} else {
print('下载出错: $e');
}
}
}
批量取消下载
CancelToken的强大之处在于,一个CancelToken实例可以关联多个请求,调用cancel方法时会取消所有关联的请求:
// 一个token可以用于多个请求
final cancelToken = CancelToken();
// 同时取消多个下载请求
await Future.wait([
dio.download(url1, savePath1, cancelToken: cancelToken),
dio.download(url2, savePath2, cancelToken: cancelToken),
dio.download(url3, savePath3, cancelToken: cancelToken),
]);
// 取消所有请求
cancelToken.cancel('批量取消所有下载');
暂停与继续下载
Dio本身并没有直接提供暂停和继续下载的API,但我们可以通过HTTP的Range请求头实现这一功能。基本思路是记录已下载的字节数,在继续下载时通过Range头指定从哪个字节开始下载。
实现暂停继续下载的核心代码
以下是实现暂停和继续下载的关键代码:
class DownloadManager {
final Dio _dio = Dio();
CancelToken? _cancelToken;
int _downloadedBytes = 0;
final String _url;
final String _savePath;
DownloadManager(this._url, this._savePath);
Future<void> startDownload() async {
_cancelToken = CancelToken();
// 检查文件是否已部分下载
final file = File(_savePath);
if (await file.exists()) {
_downloadedBytes = await file.length();
}
try {
await _dio.download(
_url,
_savePath,
options: Options(
headers: {
if (_downloadedBytes > 0)
HttpHeaders.rangeHeader: 'bytes=$_downloadedBytes-',
},
),
onReceiveProgress: (received, total) {
final totalReceived = _downloadedBytes + received;
final totalSize = _downloadedBytes + total;
final progress = (totalReceived / totalSize * 100).toStringAsFixed(0);
print('下载进度: $progress%');
},
cancelToken: _cancelToken,
// 对于断点续传,需要使用追加模式
deleteOnError: false,
lengthHeader: 'content-length',
);
} catch (e) {
if (!CancelToken.isCancel(e)) {
print('下载出错: $e');
}
}
}
void pauseDownload() {
_cancelToken?.cancel('用户暂停了下载');
_cancelToken = null;
}
bool get isPaused => _cancelToken == null;
}
断点续传的实现要点
- 记录已下载字节数:通过检查本地文件大小确定已下载的字节数
- 设置Range请求头:使用
bytes=start-格式的Range头告诉服务器从指定位置开始传输 - 文件追加模式:确保新下载的数据追加到现有文件末尾,而不是覆盖
- 正确计算进度:进度计算需要基于总文件大小和已下载字节数的总和
完整的下载管理器实现
结合前面介绍的取消、暂停和继续功能,我们可以构建一个完整的下载管理器,支持多种下载控制功能。
下载状态管理
首先定义下载可能的状态:
enum DownloadStatus {
notStarted,
downloading,
paused,
completed,
cancelled,
error,
}
完整的下载管理器类
class AdvancedDownloadManager {
final Dio _dio;
final String _url;
final String _savePath;
CancelToken? _cancelToken;
int _downloadedBytes = 0;
int _totalBytes = 0;
DownloadStatus _status = DownloadStatus.notStarted;
String? _errorMessage;
// 进度回调
final Function(double progress, DownloadStatus status)? onProgress;
AdvancedDownloadManager(
this._url,
this._savePath, {
Dio? dio,
this.onProgress,
}) : _dio = dio ?? Dio() {
_init();
}
void _init() {
_dio.interceptors.add(LogInterceptor());
_checkExistingFile();
}
// 检查是否已有部分下载的文件
Future<void> _checkExistingFile() async {
final file = File(_savePath);
if (await file.exists()) {
_downloadedBytes = await file.length();
// 获取文件总大小
await _getTotalFileSize();
if (_downloadedBytes > 0 && _downloadedBytes < _totalBytes) {
_status = DownloadStatus.paused;
onProgress?.call(_calculateProgress(), _status);
} else if (_downloadedBytes == _totalBytes) {
_status = DownloadStatus.completed;
onProgress?.call(100, _status);
}
}
}
// 获取文件总大小
Future<void> _getTotalFileSize() async {
try {
final response = await _dio.head(_url);
if (response.headers.containsKey('content-length')) {
_totalBytes = int.parse(response.headers['content-length']![0]);
}
} catch (e) {
print('获取文件大小失败: $e');
}
}
// 开始或继续下载
Future<void> start() async {
if (_status == DownloadStatus.downloading) return;
_status = DownloadStatus.downloading;
_cancelToken = CancelToken();
final file = File(_savePath);
final fileExists = await file.exists();
try {
final response = await _dio.get(
_url,
options: Options(
responseType: ResponseType.bytes,
followRedirects: false,
headers: {
if (fileExists && _downloadedBytes > 0)
HttpHeaders.rangeHeader: 'bytes=$_downloadedBytes-',
},
),
cancelToken: _cancelToken,
onReceiveProgress: (received, total) {
if (_totalBytes == 0) {
_totalBytes = total;
}
final totalReceived = _downloadedBytes + received;
final progress = totalReceived / _totalBytes;
onProgress?.call(progress * 100, _status);
},
);
// 写入文件,使用追加模式
final raf = await file.open(mode: FileMode.append);
await raf.writeFrom(response.data);
await raf.close();
_downloadedBytes += response.data.length;
if (_downloadedBytes >= _totalBytes) {
_status = DownloadStatus.completed;
onProgress?.call(100, _status);
}
} catch (e) {
if (CancelToken.isCancel(e)) {
_status = DownloadStatus.paused;
onProgress?.call(_calculateProgress(), _status);
} else {
_status = DownloadStatus.error;
_errorMessage = e.toString();
onProgress?.call(_calculateProgress(), _status);
}
}
}
// 暂停下载
void pause() {
if (_status != DownloadStatus.downloading) return;
_cancelToken?.cancel('下载已暂停');
_cancelToken = null;
_status = DownloadStatus.paused;
onProgress?.call(_calculateProgress(), _status);
}
// 取消下载并删除文件
Future<void> cancel() async {
_cancelToken?.cancel('下载已取消');
_cancelToken = null;
// 删除已下载的文件
final file = File(_savePath);
if (await file.exists()) {
await file.delete();
}
_downloadedBytes = 0;
_status = DownloadStatus.cancelled;
onProgress?.call(0, _status);
}
// 计算当前进度
double _calculateProgress() {
if (_totalBytes == 0) return 0;
return (_downloadedBytes / _totalBytes) * 100;
}
// 获取当前状态
DownloadStatus get status => _status;
// 获取错误信息
String? get errorMessage => _errorMessage;
}
使用下载管理器
void main() async {
final downloadManager = AdvancedDownloadManager(
'https://example.com/large_file.iso',
'/storage/downloads/large_file.iso',
onProgress: (progress, status) {
print('进度: ${progress.toStringAsFixed(1)}%,状态: $status');
},
);
// 开始下载
downloadManager.start();
// 5秒后暂停
await Future.delayed(Duration(seconds: 5));
downloadManager.pause();
// 3秒后继续
await Future.delayed(Duration(seconds: 3));
downloadManager.start();
// 10秒后取消
await Future.delayed(Duration(seconds: 10));
downloadManager.cancel();
}
下载管理器的高级功能
除了基本的暂停、继续和取消功能,Dio还可以与其他插件结合,实现更高级的下载管理功能。
结合Cookie管理器
在需要身份验证的下载场景中,可以结合Dio的cookie_manager插件,自动处理身份验证Cookie:
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
void setupAuthenticatedDownload() {
final dio = Dio();
final cookieJar = CookieJar();
dio.interceptors.add(CookieManager(cookieJar));
// 先进行登录获取Cookie
dio.post('/login', data: {'username': 'user', 'password': 'pass'}).then((_) {
// 登录后使用同一个dio实例进行下载,会自动带上Cookie
dio.download('https://example.com/protected_file.zip', 'local_file.zip');
});
}
下载速度限制
通过自定义拦截器,我们可以实现下载速度限制,避免下载占用过多带宽:
class SpeedLimitInterceptor extends Interceptor {
final int maxBytesPerSecond;
DateTime? _lastTime;
int _bytesSinceLastCheck = 0;
SpeedLimitInterceptor(this.maxBytesPerSecond);
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.requestOptions.responseType == ResponseType.bytes) {
_throttleSpeed(response.data.length);
}
super.onResponse(response, handler);
}
Future<void> _throttleSpeed(int bytes) async {
_bytesSinceLastCheck += bytes;
final now = DateTime.now();
if (_lastTime != null) {
final elapsed = now.difference(_lastTime!).inMilliseconds;
if (elapsed > 0) {
final bytesPerMs = _bytesSinceLastCheck / elapsed;
final bytesPerSecond = bytesPerMs * 1000;
if (bytesPerSecond > maxBytesPerSecond) {
// 计算需要延迟的时间
final excessBytes = _bytesSinceLastCheck - (maxBytesPerSecond * elapsed / 1000);
final delayMs = (excessBytes / maxBytesPerSecond) * 1000;
await Future.delayed(Duration(milliseconds: delayMs.toInt()));
}
}
}
_lastTime = now;
_bytesSinceLastCheck = 0;
}
}
// 使用速度限制拦截器
final dio = Dio();
dio.interceptors.add(SpeedLimitInterceptor(1024 * 1024)); // 限制1MB/s
实际应用场景
Flutter中的下载管理UI
在Flutter应用中,我们可以将下载管理器与UI组件结合,创建直观的下载控制界面:
class DownloadControlPanel extends StatefulWidget {
final AdvancedDownloadManager downloadManager;
const DownloadControlPanel({required this.downloadManager, Key? key}) : super(key: key);
@override
_DownloadControlPanelState createState() => _DownloadControlPanelState();
}
class _DownloadControlPanelState extends State<DownloadControlPanel> {
double _progress = 0;
DownloadStatus _status = DownloadStatus.notStarted;
@override
void initState() {
super.initState();
widget.downloadManager.onProgress = (progress, status) {
setState(() {
_progress = progress;
_status = status;
});
};
}
@override
Widget build(BuildContext context) {
return Column(
children: [
LinearProgressIndicator(value: _progress / 100),
Text('${_progress.toStringAsFixed(1)}%'),
Text('状态: ${_status.name}'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_status == DownloadStatus.downloading)
ElevatedButton(
onPressed: widget.downloadManager.pause,
child: const Text('暂停'),
),
if (_status == DownloadStatus.paused || _status == DownloadStatus.notStarted)
ElevatedButton(
onPressed: widget.downloadManager.start,
child: const Text('开始'),
),
ElevatedButton(
onPressed: widget.downloadManager.cancel,
child: const Text('取消'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
],
),
],
);
}
}
下载管理最佳实践
1. 合理设置超时时间
根据文件大小和网络条件,设置合理的超时时间:
dio.options.connectTimeout = Duration(seconds: 10);
dio.options.receiveTimeout = Duration(minutes: 5); // 下载大文件时设置较长的接收超时
2. 处理网络变化
结合Flutter的网络状态监听,在网络断开时自动暂停下载,网络恢复时提示用户继续:
import 'package:connectivity_plus/connectivity_plus.dart';
void setupNetworkMonitoring(DownloadManager manager) {
Connectivity().onConnectivityChanged.listen((result) {
if (result == ConnectivityResult.none) {
manager.pause();
} else if (result != ConnectivityResult.none && manager.status == DownloadStatus.paused) {
// 显示通知提示用户继续下载
}
});
}
3. 下载任务持久化
将下载任务信息保存到本地存储,确保应用重启后可以恢复下载状态:
// 使用shared_preferences保存下载状态
Future saveDownloadState(AdvancedDownloadManager manager) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('download_url', manager.url);
await prefs.setString('download_path', manager.savePath);
await prefs.setInt('downloaded_bytes', manager.downloadedBytes);
await prefs.setInt('total_bytes', manager.totalBytes);
await prefs.setString('download_status', manager.status.name);
}
// 恢复下载状态
Future<AdvancedDownloadManager?> restoreDownloadState() async {
final prefs = await SharedPreferences.getInstance();
final url = prefs.getString('download_url');
final path = prefs.getString('download_path');
if (url == null || path == null) return null;
final manager = AdvancedDownloadManager(url, path);
manager._downloadedBytes = prefs.getInt('downloaded_bytes') ?? 0;
manager._totalBytes = prefs.getInt('total_bytes') ?? 0;
return manager;
}
4. 避免同时进行过多下载
实现下载队列,控制同时下载的任务数量,避免资源竞争:
class DownloadQueue {
final List<AdvancedDownloadManager> _queue = [];
final int _maxConcurrentDownloads;
int _activeDownloads = 0;
DownloadQueue({int maxConcurrentDownloads = 2}) : _maxConcurrentDownloads = maxConcurrentDownloads;
void addDownload(AdvancedDownloadManager download) {
_queue.add(download);
_processQueue();
}
void _processQueue() {
if (_activeDownloads < _maxConcurrentDownloads && _queue.isNotEmpty) {
final download = _queue.removeAt(0);
_activeDownloads++;
download.onProgress = (progress, status) {
if (status == DownloadStatus.completed || status == DownloadStatus.error || status == DownloadStatus.cancelled) {
_activeDownloads--;
_processQueue(); // 处理下一个下载
}
};
download.start();
}
}
}
总结
Dio提供了强大的HTTP客户端功能,通过CancelToken和Range请求头的结合,我们可以实现功能完善的下载管理器,支持暂停、继续和取消下载操作。本文介绍的实现方案可以应用于各种Dart和Flutter应用,帮助开发者构建专业的下载体验。
通过合理使用Dio的拦截器、请求配置和取消机制,结合文件操作和状态管理,我们可以处理各种复杂的下载场景,为用户提供稳定、可靠的下载服务。
完整的示例代码可以在项目的example目录中找到,包括基础下载、取消请求和高级下载管理的实现:
更多推荐


所有评论(0)