Flutter实现资源下载断点续传的示例代码

 更新时间:2022年07月28日 14:50:08   作者:Karl_wei  
在项目开发中,特别是C端的产品,资源下载实现断点续传是非常有必要的。今天我们不讲过多原理的知识,分享下简单实用的资源断点续传

协议梳理

一般情况下,下载的功能模块,至少需要提供如下基础功能:资源下载、取消当前下载、资源是否下载成功、资源文件的大小、清除缓存文件。而断点续传主要体现在取消当前下载后,再次下载时能在之前已下载的基础上继续下载。这个能极大程度的减少我们服务器的带宽损耗,而且还能为用户减少流量,避免重复下载,提高用户体验。

前置条件:资源必须支持断点续传。如何确定可否支持?看看你的服务器是否支持Range请求即可

实现步骤

1.定好协议。我们用的http库是dio;通过校验md5检测文件缓存完整性;关于代码中的subDir,设计上认为资源会有多种:音频、视频、安装包等,每种资源分开目录进行存储。

import 'package:dio/dio.dart';

typedef ProgressCallBack = void Function(int count, int total);

typedef CancelTokenProvider = void Function(CancelToken cancelToken);

abstract class AssetRepositoryProtocol {
  /// 下载单一资源
  Future<String> downloadAsset(String url,
      {String? subDir,
      ProgressCallBack? onReceiveProgress,
      CancelTokenProvider? cancelTokenProvider,
      Function(String)? done,
      Function(Exception)? failed});

  /// 取消下载,Dio中通过CancelToken可控制
  void cancelDownload(CancelToken cancelToken);

  /// 获取文件的缓存地址
  Future<String?> filePathForAsset(String url, {String? subDir});

  /// 检查文件是否缓存成功,简单对比md5
  Future<String?> checkCachedSuccess(String url, {String? md5Str});
  
  /// 查看缓存文件的大小
  Future<int> cachedFileSize({String? subDir});

  /// 清除缓存
  Future<void> clearCache({String? subDir});
}

2.实现抽象协议,其中HttpManagerProtocol内部封装了dio的相关请求。

class AssetRepository implements AssetRepositoryProtocol {
  AssetRepository(this.httpManager);

  final HttpManagerProtocol httpManager;

  @override
  Future<String> downloadAsset(String url,
      {String? subDir,
      ProgressCallBack? onReceiveProgress,
      CancelTokenProvider? cancelTokenProvider,
      Function(String)? done,
      Function(Exception)? failed}) async {
    CancelToken cancelToken = CancelToken();
    if (cancelTokenProvider != null) {
      cancelTokenProvider(cancelToken);
    }

    final savePath = await _getSavePath(url, subDir: subDir);
    try {
      httpManager.downloadFile(
          url: url,
          savePath: savePath + '.temp',
          onReceiveProgress: onReceiveProgress,
          cancelToken: cancelToken,
          done: () {
            done?.call(savePath);
          },
          failed: (e) {
            print(e);
            failed?.call(e);
          });
      return savePath;
    } catch (e) {
      print(e);
      rethrow;
    }
  }

  @override
  void cancelDownload(CancelToken cancelToken) {
    try {
      if (!cancelToken.isCancelled) {
        cancelToken.cancel();
      }
    } catch (e) {
      print(e);
    }
  }

  @override
  Future<String?> filePathForAsset(String url, {String? subDir}) async {
    final path = await _getSavePath(url, subDir: subDir);
    final file = File(path);
    if (!(await file.exists())) {
      return null;
    }
    return path;
  }

  @override
  Future<String?> checkCachedSuccess(String url, {String? md5Str}) async {
    String? path = await _getSavePath(url, subDir: FileType.video.dirName);
    bool isCached = await File(path).exists();
    if (isCached && (md5Str != null && md5Str.isNotEmpty)) {
      // 存在但是md5验证不通过
      File(path).readAsBytes().then((Uint8List str) {
        if (md5.convert(str).toString() != md5Str) {
          path = null;
        }
      });
    } else if (isCached) {
      return path;
    } else {
      path = null;
    }
    return path;
  }
  
  @override
  Future<int> cachedFileSize({String? subDir}) async {
    final dir = await _getDir(subDir: subDir);
    if (!(await dir.exists())) {
      return 0;
    }

    int totalSize = 0;
    await for (var entity in dir.list(recursive: true)) {
      if (entity is File) {
        try {
          totalSize += await entity.length();
        } catch (e) {
          print('Get size of $entity failed with exception: $e');
        }
      }
    }

    return totalSize;
  }

  @override
  Future<void> clearCache({String? subDir}) async {
    final dir = await _getDir(subDir: subDir);
    if (!(await dir.exists())) {
      return;
    }
    dir.deleteSync(recursive: true);
  }

  Future<String> _getSavePath(String url, {String? subDir}) async {
    final saveDir = await _getDir(subDir: subDir);

    if (!saveDir.existsSync()) {
      saveDir.createSync(recursive: true);
    }

    final uri = Uri.parse(url);
    final fileName = uri.pathSegments.last;
    return saveDir.path + fileName;
  }

  Future<Directory> _getDir({String? subDir}) async {
    final cacheDir = await getTemporaryDirectory();
    late final Directory saveDir;
    if (subDir == null) {
      saveDir = cacheDir;
    } else {
      saveDir = Directory(cacheDir.path + '/$subDir/');
    }
    return saveDir;
  }
}

3.封装dio下载,实现资源断点续传。

这里的逻辑比较重点,首先未缓存100%的文件,我们以.temp后缀进行命名,在每次下载时检测下是否有.temp的文件,拿到其文件字节大小;传入在header中的range字段,服务器就会去解析需要从哪个位置继续下载;下载全部完成后,再把文件名改回正确的后缀即可。

final downloadDio = Dio();

Future<void> downloadFile({
  required String url,
  required String savePath,
  required CancelToken cancelToken,
  ProgressCallback? onReceiveProgress,
  void Function()? done,
  void Function(Exception)? failed,
}) async {
  int downloadStart = 0;
  File f = File(savePath);
  if (await f.exists()) {
    // 文件存在时拿到已下载的字节数
    downloadStart = f.lengthSync();
  }
  print("start: $downloadStart");
  try {
    var response = await downloadDio.get<ResponseBody>(
      url,
      options: Options(
        /// Receive response data as a stream
        responseType: ResponseType.stream,
        followRedirects: false,
        headers: {
          /// 加入range请求头,实现断点续传
          "range": "bytes=$downloadStart-",
        },
      ),
    );
    File file = File(savePath);
    RandomAccessFile raf = file.openSync(mode: FileMode.append);
    int received = downloadStart;
    int total = await _getContentLength(response);
    Stream<Uint8List> stream = response.data!.stream;
    StreamSubscription<Uint8List>? subscription;
    subscription = stream.listen(
      (data) {
        /// Write files must be synchronized
        raf.writeFromSync(data);
        received += data.length;
        onReceiveProgress?.call(received, total);
      },
      onDone: () async {
        file.rename(savePath.replaceAll('.temp', ''));
        await raf.close();
        done?.call();
      },
      onError: (e) async {
        await raf.close();
        failed?.call(e);
      },
      cancelOnError: true,
    );
    cancelToken.whenCancel.then((_) async {
      await subscription?.cancel();
      await raf.close();
    });
  } on DioError catch (error) {
    if (CancelToken.isCancel(error)) {
      print("Download cancelled");
    } else {
      failed?.call(error);
    }
  }
}

写在最后

这篇文章确实没有技术含量,水一篇,但其实是实用的。这个断点续传的实现有几个注意的点:

  • 使用文件操作的方式,区分后缀名来管理缓存的资源;
  • 安全性使用md5校验,这点非常重要,断点续传下载的文件,在完整性上可能会因为各种突发情况而得不到保障;
  • 在资源管理协议上,我们将下载、检测、获取大小等方法都抽象出去,在业务调用时比较灵活。

以上就是Flutter实现资源下载断点续传的示例代码的详细内容,更多关于Flutter资源下载断点续传的资料请关注脚本之家其它相关文章!

相关文章

  • Android仿小米安全中心检测进度条效果

    Android仿小米安全中心检测进度条效果

    这篇文章主要介绍了Android仿小米安全中心检测进度条效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • Android利用zxing快速集成二维码扫描的实例教程

    Android利用zxing快速集成二维码扫描的实例教程

    最近二维码真是越来越火了,随便电视上、网络上、商场里,到处都是二维码,所以下面这篇文章我们就来给大家介绍关于Android利用zxing快速集成二维码扫描的相关资料,需要的朋友可以参考借鉴,下面随着小编来一起看看吧。
    2017-09-09
  • Android-Zxing实现二维码的扫描与生成

    Android-Zxing实现二维码的扫描与生成

    本文主要介绍了Android中Zxing实现二维码的扫描与生成的方法,具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • Android Studio debug.keystore位置介绍

    Android Studio debug.keystore位置介绍

    这篇文章主要介绍了Android Studio debug.keystore位置,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-03-03
  • Android 监听网络状态方法详解

    Android 监听网络状态方法详解

    这篇文章主要介绍了Android 监听网络状态方法详解的相关资料,需要的朋友可以参考下
    2017-07-07
  • Ubuntu Android源码以及内核下载与编译

    Ubuntu Android源码以及内核下载与编译

    本文主要介绍Android源码的下载和编译,这里整理了相关资料及如何下载和编译的详细步骤,有需要的小伙伴可以参考下
    2016-09-09
  • Android网络工具类NetworkUtils详解

    Android网络工具类NetworkUtils详解

    这篇文章主要为大家详细介绍了Android网络工具类NetworkUtils,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-04-04
  • 浮动AppBar中的textField焦点回滚问题解决

    浮动AppBar中的textField焦点回滚问题解决

    这篇文章主要为大家介绍了浮动AppBar中的textField焦点回滚问题解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Android 沉浸式状态栏及悬浮效果

    Android 沉浸式状态栏及悬浮效果

    这篇文章主要介绍了Android 沉浸式状态栏及悬浮效果的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-11-11
  • Android EditText被软键盘遮盖的处理方法

    Android EditText被软键盘遮盖的处理方法

    android app新增了透明栏效果,结果发现键盘弹起后会遮盖屏幕底部的EditText,没有像想象中的调整窗口大小,并滚动ScrollView,将EditText显示在键盘上方。下面小编把解决方法记录一下,特此分享到脚本之家平台,感兴趣的朋友一起看看吧
    2016-10-10

最新评论