Dio下载管理器:暂停、继续与取消下载

【免费下载链接】dio A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc. 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/di/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;
}

断点续传的实现要点

  1. 记录已下载字节数:通过检查本地文件大小确定已下载的字节数
  2. 设置Range请求头:使用bytes=start-格式的Range头告诉服务器从指定位置开始传输
  3. 文件追加模式:确保新下载的数据追加到现有文件末尾,而不是覆盖
  4. 正确计算进度:进度计算需要基于总文件大小和已下载字节数的总和

完整的下载管理器实现

结合前面介绍的取消、暂停和继续功能,我们可以构建一个完整的下载管理器,支持多种下载控制功能。

下载状态管理

首先定义下载可能的状态:

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目录中找到,包括基础下载、取消请求和高级下载管理的实现:

【免费下载链接】dio A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc. 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/di/dio

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐