Flutter不依赖插件实现文件下载

与前文“文件上传“相似,当你去搜索引擎搜“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回调,分别在下载中、下载完成、下载出错时调用。实现文件下载的基本流程如下:

  1. 向服务器下载文件的接口发送请求
  2. 拿到文件名并创建对应文件
  3. 监听下载事件,在下载时写入文件
  4. 下载完成后释放资源

下面用代码一步步实现上述流程。

一、向服务器下载文件的接口发送请求

跟普通的接口请求一样,没有特别的地方。

final request = await HttpClient().openUrl(method, Uri.http(host, path));
final response = await request.close();

二、拿到文件名并创建对应文件

文件名处于响应Headercontent-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监听流数据下载事件,一收到数据就向文件中写入,并输出下载进度。

为了能在下载过程中获取下载进度,需要声明下面两个变量:

  1. current:记录已下载数据大小
  2. 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}');
    },
  );
}

留下评论

邮箱地址不会被公开。 必填项已用*标注

给博主打赏

2元 5元 10元