与前文“文件上传“相似,当你去搜索引擎搜“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}'); }, ); }