与前文“文件上传“相似,当你去搜索引擎搜“Flutter 文件下载”时,得到的结果几乎都是用插件来实现的。但其实Dart已经提供了相关的API,手动实现也不复杂。
在Flutter中向服务器发送Http请求,并获取响应的json,对应代码如下:
Future http (final String method, final String host, final String path) async {
/// 构建请求
final request = await HttpClient().openUrl(method, Uri.http(host, path));
/// 发送请求并获取服务器响应结果
final response = await request.close();
/// 用utf8解码器处理服务器响应的流数据
final res = await response.transform(Utf8Decoder()).join();
/// 字符串转json对象
final json = jsonDecode(res);
/// 根据http状态码决定返回状态
return response.statusCode == 200 ? json : Future.error(json);
}
注意:这里所用的
HttpClient对象属于dart:io库下的内容,在web平台是无法使用的。
如果要实现文件下载,就不能通过utf8解码器来处理服务器响应的流数据,而是改用listen方法监听响应的流数据,该方法提供了onData、onDone、onError回调,分别在下载中、下载完成、下载出错时调用。实现文件下载的基本流程如下:
- 向服务器下载文件的接口发送请求
- 拿到文件名并创建对应文件
- 监听下载事件,在下载时写入文件
- 下载完成后释放资源
下面用代码一步步实现上述流程。
一、向服务器下载文件的接口发送请求
跟普通的接口请求一样,没有特别的地方。
final request = await HttpClient().openUrl(method, Uri.http(host, path)); final response = await request.close();
二、拿到文件名并创建对应文件
文件名处于响应Header的content-disposition字段中:
... content-disposition: attachment; filename=test-download.zip ...
可以通过response.headers.value('content-disposition')拿到该字段值,再用正则匹配filename=后面的内容,就能拿到文件名了。
除此之外,我们应该把文件储存在应用程序的专属目录,这里以Android为例,放到应用的缓存目录,externalCacheDir方法需要自行在Android端实现。
/// 拿到应用专属的缓存目录
final dir = await MethodChannel('native').invokeMethod('externalCacheDir');
/// 通过正则匹配响应Header中的文件名,若匹配不到用temp代替
final name = RegExp(r'filename=([^;]*)').firstMatch(response.headers.value('content-disposition') ?? '')?.group(1) ?? 'temp';
/// 在缓存目录创建对应的文件
final file = File('$dir/$name');
/// 拿到文件的写入流
final io = file.openWrite();
三、监听下载事件,在下载时写入文件
调用response.listen方法,传入onData监听流数据下载事件,一收到数据就向文件中写入,并输出下载进度。
为了能在下载过程中获取下载进度,需要声明下面两个变量:
current:记录已下载数据大小total:记录文件总大小
var current = 0;
final total = response.contentLength;
response.listen(
/// 下载中
(event) {
/// 写入文件
io.add(event);
/// 记录已下载数据大小
current += event.length;
/// 已下载大小 / 总大小 = 进度
print('downloading: ${(current / total * 100).toStringAsFixed(2)}%');
},
);
四、下载完成后释放资源
listen方法中传入onDone监听文件下载完成事件,下载完成后不需要再对文件进行任何操作,因此可以释放IO产生的缓存以及关闭文件的写入流。
response.listen(
/// onData
/// 下载完成后
onDone: () async {
/// 释放缓存
await io.flush();
/// 关闭写入流
await io.close();
print('downloaded ${file.path}');
},
);
完整代码
Future downloadFile (final String method, final String host, final String path) async {
/// 向文件接口发送请求
final request = await HttpClient().openUrl(method, Uri.http(host, path));
final response = await request.close();
/// 创建对应文件
final dir = await MethodChannel('native').invokeMethod('externalCacheDir');
final name = RegExp(r'filename=([^;]*)').firstMatch(response.headers.value('content-disposition') ?? '')?.group(1) ?? 'temp';
final file = File('$dir/$name');
final io = file.openWrite();
/// 监听下载事件
var current = 0;
final total = response.contentLength;
response.listen(
/// 下载中:写入下载数据,并显示下载进度
(event) {
io.add(event);
current += event.length;
print('downloading: ${(current / total * 100).toStringAsFixed(2)}%');
},
/// 下载完成:释放资源
onDone: () async {
await io.flush();
await io.close();
print('downloaded ${file.path}');
},
);
}