Flutter实现资源下载断点续传的示例代码
协议梳理
一般情况下,下载的功能模块,至少需要提供如下基础功能:资源下载、取消当前下载、资源是否下载成功、资源文件的大小、清除缓存文件。而断点续传主要体现在取消当前下载后,再次下载时能在之前已下载的基础上继续下载。这个能极大程度的减少我们服务器的带宽损耗,而且还能为用户减少流量,避免重复下载,提高用户体验。
前置条件:资源必须支持断点续传。如何确定可否支持?看看你的服务器是否支持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.包哪里来,动动手指就能领)。
看下图所示是好多参与这次活动领取红包的朋友: