[译]Flutter文件选取器组件file_picker(三)常见问题

2,314 阅读4分钟

「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战」。

本文翻译自: FAQ · miguelpruivo/flutter_file_picker Wiki · GitHub

版本: file_picker 4.3.3


问:为什么使用了过滤器还能选取其它文件类型,或者为什么它完全没过滤?

我在使用 FileType.custom ,同时指定了一些扩展名(例:[csv, jpg]),然后文件浏览器并不遵循这个,允许我选取其它类型。

答:  当设置了自定义的扩展,这些会转换为 MIME 类型,然后,会向操作系统发送一个 intent 来请求文件浏览器来处理这些,它可以让你用指定的限制选取文件 - 典型的来说是原生的那个。

尽管这样,一些 Android OS 版本 (定制ROM)并不总是带有原生的文件浏览器,或者你不无法阻止用户安装第三方的文件浏览器(如ESExplorer)。这些文件浏览器可能会处理请求但 并不总是 (实际上,很不幸的是,大多数情况都不是) 遵循 MIME 限制,这是由于它们需要实现这种特性,并且没有方法可以绕过。所以,不能保证 fileExtensions 会被遵循 ,并且当你经历该问题时,在报告之前,尝试官方的模拟器看一下能否再现。

所以,由于这种情况,当用户选取了目标外的扩展名,该库应该必须抛出异常,或者开发者可以在应用中添加附加的保护,正如你循环访问选取的文件时,显示一个自定义警告或类似内容会使其看上去更合理。

更多内容查看这里


问:为什么无法访问原始文件路径?

答: 这是常被问到的问题之一,实际上它已足够使我创建这个 FAQ 部分,因为该问题并不代表符合问题解决方案的实际问题。

在 Android 上,原始路径在 file_picker 2.0.0 之前是可用的。尽管如此,在 iOS 上,它 完全 不可用,这是由于 iOS 希望你创建缓存的副本然后对副本进行操作。但是,2.0.0 引进了分区存储支持 (Android 10) ,并且每个 Android 文档建议文件应该以两种方式访问:

  1. 通过文件 URI 为 CRUD 操作选取文件(读、删、编集)并直接使用。 — 这是你实际上想要的但不幸的是,Flutter 并不支持,这是由于它需要绝对路径来打开一个文件描述符。
  2. 暂缓存文件用于上传或类似处理,或者只是复制到应用的永久存储中,这样之后可能访问它 - 现在已经是这样做的。即使这样,在第一次选取时,你可能需要一个附加步骤移动或复制文件,使其更安全可靠地在任何 Android 设备上访问允许的文件。

可以在 Commonsware 上查看该博客 获取更多信息。

TL;DR(太长可以不看):在第一次选取文件之后,只需要移动到应用的存储分区并在那里进一步操作(可以使用 path_provider 库的 getApplicationDocumentsDirectory() 方法来获得应用的本地存储)

考虑到如果两次选取了同一个文件(相同的文件名),现在 file_picker 已使该问题最优化了。 你可能会选取了之前缓存的文件,因为它只会在第一次被选取时缓存。


问: 如何得知用户取消了选择?

答:  在 dart:io 设备 (Android 、 iOS 和 Desktop) 上,如果用户取消或关闭了选择器,FilePickerResult 会自动返回 null 。

尽管如此,在 Web 上,现在还有一个局限是没有方法来知道用户是否取消,这是因为在平台终端它们没有触发任何事件。


问: 如何在 Web 上访问 path

答:  在浏览器上是无法访问路径的,因为浏览器提供的是假的路径。如果你想创建 File 实例并上传到某个地方,如 FireStorage ,你可以直接使用 bytes 来实现。

// 取得文件
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false);

if (result != null && result.files.isNotEmpty) {
  final fileBytes = result.files.first.bytes;
  final fileName = result.files.first.name;
  
  // 上传文件
  await FirebaseStorage.instance.ref('uploads/$fileName').putData(fileBytes);
}

感谢 @devj3ns 提供示例。


问: 如何使用 withReadStream

答: 如果你需要选取一个大的文件上传到某个地方,可以利用 withReadStream 然后按块读取。查看下面的示例。

import 'package:file_picker/file_picker.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:mime/mime.dart';

void main() async {
  final result = await FilePicker.platform.pickFiles(
    type: FileType.custom,
    allowedExtensions: [
      'jpg',
      'png',
      'mp4',
      'webm',
    ],
    withData: false,
    withReadStream: true,
  );

  if (result == null || result.files.isEmpty) {
    throw Exception('No files picked or file picker was canceled');
  }

  final file = result.files.first;
  final filePath = file.path;
  final mimeType = filePath != null ? lookupMimeType(filePath) : null;
  final contentType = mimeType != null ? MediaType.parse(mimeType) : null;

  final fileReadStream = file.readStream;
  if (fileReadStream == null) {
    throw Exception('Cannot read file from null stream');
  }
  final stream = http.ByteStream(fileReadStream);

  final uri = Uri.https('siasky.net', '/skynet/skyfile');
  final request = http.MultipartRequest('POST', uri);
  final multipartFile = http.MultipartFile(
    'file',
    stream,
    file.size,
    filename: file.name,
    contentType: contentType,
  );
  request.files.add(multipartFile);

  final httpClient = http.Client();
  final response = await httpClient.send(request);

  if (response.statusCode != 200) {
    throw Exception('HTTP ${response.statusCode}');
  }

  final body = await response.stream.transform(utf8.decoder).join();

  print(body);
}

感谢 @redsolver 提供示例。