当前位置:首页 > 谈天说地 > 正文内容

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

34资源网2022年07月29日 10:11300

协议梳理

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

前置条件:资源必须支持断点续传。如何确定可否支持?看看你的服务器是否支持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资源下载断点续传的资料请关注萬仟网其它相关文章!

看完文章,还可以用支付宝扫描下面的二维码领取一个支付宝红包,目前可领1-88元不等

支付宝红包二维码

除了扫码可以领取之外,大家还可以(复制 720087999 打开✔支付宝✔去搜索, h`o`n.g.包哪里来,动动手指就能领)。

看下图所示是好多参与这次活动领取红包的朋友:

支付宝红包

扫描二维码推送至手机访问。

版权声明:本文由34楼发布,如需转载请注明出处。

本文链接:https://www.34l.com/post/20000.html

分享给朋友:

相关文章

全职妈妈自己创业干点什么好?适合全职妈妈创业的项目分享
全职妈妈自己创业干点什么好?适合全职妈妈创业的项目分享

作为全职妈妈的你肯定还在担心家里的开支问题吧?因为生活压力太大,很多全职妈妈想着在家照顾孩子的同时想通过自己的努力创业。那么,全职妈妈自己创业干点什么好呢?下面,小编整理了四个适合全职妈妈创业的项目,大家一起来看看吧。1、做任务赚钱如果你是...

​分享5句用薪尽火灭成语造句
​分享5句用薪尽火灭成语造句

1、入涅盘又称入灭、薪尽火灭(薪喻佛身或机缘,火喻智慧或佛身)。2、否则,咱师徒情分从此刻开始薪尽火灭。3、铲除这些非正常因素,降温楼市才能“薪尽火灭”。4、薪尽火灭:咱们的观点既是云云不同,那麼从今以后你别再来见我,从这个时候起薪尽火灭好...

互联网公司好日子到头,逻辑彻底变了
互联网公司好日子到头,逻辑彻底变了

好日子到头了,逻辑彻底变了,互联网公司已经不再是香饽饽。有两个重要的信号。一是资本不能无序扩张;二是互联网平台税率上调;前者直接宣布现在的那些玩家,你们继续玩,这没关系。但是想要通过资本野蛮扩大,不公平竞争,这就甭想了。后者直接影响到了互联...

抹去了怎么造句,分享26句用抹去了造句
抹去了怎么造句,分享26句用抹去了造句

(1) 夜色抹去了最后一缕残阳,夜幕就像剧场里的绒幕,慢慢落下来了。(2) 回忆与现实的区别就是,回忆只留下了静静的画面,而抹去了喧闹的声音。李宫俊 (3) 你的回归喻示着团圆,你的回归代表着喜庆,你的回归抹去了屈辱,你的回归载满...

想要赚钱创业首先要改变自己的思维
想要赚钱创业首先要改变自己的思维

大市场,前景行业,无非是能源、通信、金融行业。小市场,比较有前景的,是大众所需,消费忠识度比较高的行业,比如饮食,零售、生产。无论是工作,还是创业!你需要选择自己兴趣,找准自己的优势,发现你的特长.1:考虑你的兴趣,做你最喜欢做的,只有让工...

抖音长视频怎么开通?抖音开通长视频的权限分享
抖音长视频怎么开通?抖音开通长视频的权限分享

经常玩抖音的朋友来说开通抖音1分钟长视频已经不是一件难事了。大家都知道抖音默认视频长度为15秒。只有达到一定要求才可以获得长视频权限。但是现在我们发现能发抖音长视频的朋友越来越多了。他们是怎么做到的呢?抖音怎么发长1分钟视频呢?抖音长视频是...