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资源下载断点续传的资料请关注萬仟网其它相关文章!
看完文章,还可以扫描下面的二维码下载快手极速版领4元红包
除了扫码领红包之外,大家还可以在快手极速版做签到,看视频,做任务,参与抽奖,邀请好友赚钱)。
邀请两个好友奖最高196元,如下图所示:







