Flutter for OpenHarmony:引入第三方库file_selector 文件选择详解
文件选择是移动应用中非常常见的功能,无论是上传图片、导入配置文件、选择文档还是批量处理媒体文件,都需要使用到文件选择器。在 Flutter for OpenHarmony 应用开发中,是一个功能强大的文件选择插件,提供了完整的文件选择和管理功能,支持多种文件类型过滤和自定义配置。label: 'images', // 分组标签extensions: <String>['jpg', 'png'],
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、file_selector 组件概述
文件选择是移动应用中非常常见的功能,无论是上传图片、导入配置文件、选择文档还是批量处理媒体文件,都需要使用到文件选择器。在 Flutter for OpenHarmony 应用开发中,file_selector 是一个功能强大的文件选择插件,提供了完整的文件选择和管理功能,支持多种文件类型过滤和自定义配置。
📋 file_selector 组件特点
| 特点 | 说明 |
|---|---|
| 单文件选择 | 支持选择单个文件 |
| 多文件选择 | 支持一次选择多个文件 |
| 目录选择 | 支持选择目录路径 |
| 文件类型过滤 | 支持扩展名、MIME 类型等多种过滤方式 |
| 初始目录设置 | 支持设置文件选择器的初始打开目录 |
| 自定义按钮文本 | 支持自定义确认按钮文本 |
| 跨平台兼容 | 支持 Android、iOS、Web、Windows、Linux、macOS |
| 鸿蒙适配 | 专门为 OpenHarmony 平台进行了适配,API 12+ |
文件类型过滤支持
| 过滤类型 | 说明 | Android | iOS | Linux | macOS | Windows | Web | OpenHarmony |
|---|---|---|---|---|---|---|---|---|
| extensions | 文件扩展名(如 jpg、png) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| mimeTypes | MIME 类型(如 image/jpeg) | ✔️ | ✔️ | ✔️† | ✔️ | ✔️ | ||
| uniformTypeIdentifiers | 统一类型标识符(iOS/macOS) | ✔️ | ✔️ | ✔️ | ||||
| webWildCards | Web 通配符(如 image/*) | ✔️ | ✔️ |
† macOS 11 (Big Sur) 及以上版本支持 mimeTypes。
💡 使用场景:图片上传、文档导入、批量处理、文件管理、媒体播放器等。
二、OpenHarmony 平台适配说明
2.1 权限要求
file_selector 本身不需要特殊权限。但如果选择网络资源或进行网络操作,需要配置网络权限。
在 entry 目录下的 module.json5 中添加权限
打开 ohos/entry/src/main/module.json5,在 requestPermissions 数组中添加:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
在 entry 目录下添加申请权限的原因
打开 ohos/entry/src/main/resources/base/element/string.json,在 string 数组中添加:
{
"name": "network_reason",
"value": "使用网络访问文件资源"
}
| 权限名称 | 权限等级 | 用途 |
|---|---|---|
| ohos.permission.INTERNET | normal | 访问网络资源 |
⚠️ 注意:
- 文件选择器使用的是系统原生文件选择对话框,会自动处理文件访问权限
- 权限配置必须包含
reason和usedScene字段,否则会导致应用安装失败
三、项目配置与安装
3.1 添加依赖配置
首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 file_selector 依赖。
打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:
dependencies:
flutter:
sdk: flutter
# 添加 file_selector 依赖(OpenHarmony 适配版本)
file_selector:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages
path: packages/file_selector/file_selector
3.2 下载依赖
配置完成后,需要在项目根目录执行以下命令下载依赖:
flutter pub get
3.3 导入库
使用前需要导入相关库:
import 'package:file_selector/file_selector.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
注意:在 OpenHarmony 平台上,需要在 main.dart 中设置平台实例:
import 'package:file_selector_ohos/file_selector_ohos.dart';
void main() {
FileSelectorPlatform.instance = FileSelectorOhos();
runApp(const MyApp());
}
四、file_selector 基础用法
4.1 XTypeGroup 文件类型组
XTypeGroup 用于定义可选择的文件类型,支持多种过滤方式:
基本定义
const XTypeGroup typeGroup = XTypeGroup(
label: 'images', // 分组标签
extensions: <String>['jpg', 'png'], // 文件扩展名
);
完整参数
const XTypeGroup typeGroup = XTypeGroup(
label: 'images', // 分组标签(可选)
extensions: <String>['jpg', 'png'], // 文件扩展名(可选)
mimeTypes: <String>['image/jpeg'], // MIME 类型(可选)
uniformTypeIdentifiers: <String>['public.image'], // 统一类型标识符(可选)
webWildCards: <String>['image/*'], // Web 通配符(可选)
);
多个类型组
可以定义多个类型组,用户可以在不同类型组之间选择:
const XTypeGroup jpgsTypeGroup = XTypeGroup(
label: 'JPEGs',
extensions: <String>['jpg', 'jpeg'],
uniformTypeIdentifiers: <String>['public.jpeg'],
);
const XTypeGroup pngTypeGroup = XTypeGroup(
label: 'PNGs',
extensions: <String>['png'],
uniformTypeIdentifiers: <String>['public.png'],
);
4.2 选择单个文件
使用 openFile 方法选择单个文件:
Future<void> _openFile() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png'],
uniformTypeIdentifiers: <String>['public.image'],
);
final XFile? file = await FileSelectorPlatform.instance.openFile(
acceptedTypeGroups: <XTypeGroup>[typeGroup],
initialDirectory: '/storage/emulated/0/Download', // 初始目录(可选)
confirmButtonText: '选择图片', // 确认按钮文本(可选)
);
if (file == null) {
// 用户取消了选择
return;
}
// 处理选中的文件
final String fileName = file.name;
final Uint8List bytes = await file.readAsBytes();
final String path = file.path;
print('文件名: $fileName');
print('文件路径: $path');
print('文件大小: ${bytes.length} bytes');
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| acceptedTypeGroups | List <XTypeGroup>? |
可选择的文件类型组 |
| initialDirectory | String? | 初始打开的目录路径 |
| confirmButtonText | String? | 确认按钮的文本 |
4.3 选择多个文件
使用 openFiles 方法选择多个文件:
Future<void> _openMultipleFiles() async {
const XTypeGroup jpgsTypeGroup = XTypeGroup(
label: 'JPEGs',
extensions: <String>['jpg', 'jpeg'],
uniformTypeIdentifiers: <String>['public.jpeg'],
);
const XTypeGroup pngTypeGroup = XTypeGroup(
label: 'PNGs',
extensions: <String>['png'],
uniformTypeIdentifiers: <String>['public.png'],
);
final List<XFile> files = await FileSelectorPlatform.instance.openFiles(
acceptedTypeGroups: <XTypeGroup>[
jpgsTypeGroup,
pngTypeGroup,
],
initialDirectory: '/storage/emulated/0/Pictures',
confirmButtonText: '选择图片',
);
if (files.isEmpty) {
// 用户取消了选择
return;
}
// 处理选中的多个文件
for (final XFile file in files) {
print('文件名: ${file.name}');
print('文件路径: ${file.path}');
}
}
4.4 选择目录
⚠️ 重要说明:在原始的 file_selector_ohos 插件中,
getDirectoryPath方法并未实现(在FileSelectorApiImpl.ets中直接抛出了Method not implemented错误),导致点击选择目录无响应。为了使用该功能,需要手动实现以下代码。详情请看8.4章节
使用 getDirectoryPath 方法选择目录:
Future<void> _selectDirectory() async {
final String? directoryPath = await FileSelectorPlatform.instance.getDirectoryPath(
initialDirectory: '/storage/emulated/0/Download',
confirmButtonText: '选择目录',
);
if (directoryPath == null) {
// 用户取消了选择
return;
}
print('选择的目录: $directoryPath');
}
⚠️ 注意:OpenHarmony 不支持选择保存位置(
getSaveLocation),仅支持选择目录。
五、XFile 文件对象
XFile 是表示选中文件的类,提供了丰富的文件操作方法:
5.1 常用属性
final XFile file = ...;
// 文件名
String fileName = file.name;
// 文件路径
String path = file.path;
// 文件大小(字节)
int length = await file.length();
// MIME 类型
String? mimeType = file.mimeType;
5.2 读取文件内容
读取为字节数组
final Uint8List bytes = await file.readAsBytes();
读取为字符串
final String content = await file.readAsString();
读取为流
final Stream<Uint8List> stream = file.openRead();
5.3 显示图片
// 读取图片字节
final Uint8List bytes = await file.readAsBytes();
// 显示图片
Image.memory(bytes);
六、完整示例应用

下面是一个功能完整的文件选择应用,包含单文件选择、多文件选择和目录选择:
import 'dart:typed_data';
import 'package:file_selector/file_selector.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
import 'package:file_selector_ohos/file_selector_ohos.dart';
import 'package:flutter/material.dart';
void main() {
FileSelectorPlatform.instance = FileSelectorOhos();
runApp(const FileSelectorApp());
}
class FileSelectorApp extends StatelessWidget {
const FileSelectorApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'File Selector Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF8B5CF6)),
useMaterial3: true,
),
home: const FileSelectorHomePage(),
);
}
}
class FileSelectorHomePage extends StatelessWidget {
const FileSelectorHomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('文件选择示例'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildFeatureCard(
context,
icon: Icons.image,
title: '选择单个图片',
subtitle: '打开文件选择器选择一张图片',
onTap: () => _openSingleImage(context),
),
const SizedBox(height: 16),
_buildFeatureCard(
context,
icon: Icons.photo_library,
title: '选择多个图片',
subtitle: '打开文件选择器选择多张图片',
onTap: () => _openMultipleImages(context),
),
const SizedBox(height: 16),
_buildFeatureCard(
context,
icon: Icons.text_snippet,
title: '选择文本文件',
subtitle: '打开文件选择器选择文本文件',
onTap: () => _openTextFile(context),
),
const SizedBox(height: 16),
_buildFeatureCard(
context,
icon: Icons.folder,
title: '选择目录',
subtitle: '打开目录选择器选择文件夹',
onTap: () => _selectDirectory(context),
),
],
),
);
}
Widget _buildFeatureCard(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return Card(
elevation: 2,
child: ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFF8B5CF6).withOpacity(0.1),
child: Icon(icon, color: const Color(0xFF8B5CF6)),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
Future<void> _openSingleImage(BuildContext context) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png'],
uniformTypeIdentifiers: <String>['public.image'],
);
final XFile? file = await FileSelectorPlatform.instance.openFile(
acceptedTypeGroups: <XTypeGroup>[typeGroup],
confirmButtonText: '选择图片',
);
if (file == null) return;
final Uint8List bytes = await file.readAsBytes();
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) => _ImageDisplay(file.name, bytes),
);
}
}
Future<void> _openMultipleImages(BuildContext context) async {
const XTypeGroup jpgsTypeGroup = XTypeGroup(
label: 'JPEGs',
extensions: <String>['jpg', 'jpeg'],
uniformTypeIdentifiers: <String>['public.jpeg'],
);
const XTypeGroup pngTypeGroup = XTypeGroup(
label: 'PNGs',
extensions: <String>['png'],
uniformTypeIdentifiers: <String>['public.png'],
);
final List<XFile> files = await FileSelectorPlatform.instance.openFiles(
acceptedTypeGroups: <XTypeGroup>[jpgsTypeGroup, pngTypeGroup],
confirmButtonText: '选择图片',
);
if (files.isEmpty) return;
final List<Uint8List> imageBytes = <Uint8List>[];
for (final XFile file in files) {
imageBytes.add(await file.readAsBytes());
}
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) => _MultipleImagesDisplay(imageBytes),
);
}
}
Future<void> _openTextFile(BuildContext context) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'text',
extensions: <String>['txt', 'json'],
uniformTypeIdentifiers: <String>['public.text'],
);
final XFile? file = await FileSelectorPlatform.instance.openFile(
acceptedTypeGroups: <XTypeGroup>[typeGroup],
confirmButtonText: '选择文件',
);
if (file == null) return;
final String fileName = file.name;
final Uint8List bytes = await file.readAsBytes();
final String fileContent = String.fromCharCodes(bytes);
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) => _TextDisplay(fileName, fileContent),
);
}
}
Future<void> _selectDirectory(BuildContext context) async {
final String? directoryPath = await FileSelectorPlatform.instance.getDirectoryPath(
confirmButtonText: '选择目录',
);
if (directoryPath == null) return;
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) => _DirectoryDisplay(directoryPath),
);
}
}
}
class _ImageDisplay extends StatelessWidget {
final String fileName;
final Uint8List bytes;
const _ImageDisplay(this.fileName, this.bytes);
Widget build(BuildContext context) {
return AlertDialog(
title: Text(fileName),
content: SizedBox(
width: 300,
height: 300,
child: Image.memory(bytes, fit: BoxFit.contain),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
);
}
}
class _MultipleImagesDisplay extends StatelessWidget {
final List<Uint8List> fileBytes;
const _MultipleImagesDisplay(this.fileBytes);
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('图片画廊'),
content: SizedBox(
width: 400,
height: 300,
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: fileBytes.length,
itemBuilder: (context, index) {
return Image.memory(
fileBytes[index],
fit: BoxFit.cover,
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
);
}
}
class _TextDisplay extends StatelessWidget {
final String fileName;
final String fileContent;
const _TextDisplay(this.fileName, this.fileContent);
Widget build(BuildContext context) {
return AlertDialog(
title: Text(fileName),
content: SizedBox(
width: 400,
height: 300,
child: Scrollbar(
child: SingleChildScrollView(
child: Text(fileContent),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
);
}
}
class _DirectoryDisplay extends StatelessWidget {
final String directoryPath;
const _DirectoryDisplay(this.directoryPath);
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('选择的目录'),
content: Text(directoryPath),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
);
}
}
七、API 参考
7.1 FileSelectorPlatform 主要方法
| 方法 | 返回值 | 说明 |
|---|---|---|
| openFile({…}) | Future<XFile?> | 打开文件选择器选择单个文件 |
| openFiles({…}) | Future<List <XFile>> |
打开文件选择器选择多个文件 |
| getDirectoryPath({…}) | Future<String?> | 打开目录选择器选择目录 |
openFile 参数
| 参数 | 类型 | 说明 |
|---|---|---|
| acceptedTypeGroups | List <XTypeGroup>? |
可选择的文件类型组 |
| initialDirectory | String? | 初始打开的目录路径 |
| confirmButtonText | String? | 确认按钮的文本 |
openFiles 参数
| 参数 | 类型 | 说明 |
|---|---|---|
| acceptedTypeGroups | List <XTypeGroup>? |
可选择的文件类型组 |
| initialDirectory | String? | 初始打开的目录路径 |
| confirmButtonText | String? | 确认按钮的文本 |
getDirectoryPath 参数
| 参数 | 类型 | 说明 |
|---|---|---|
| initialDirectory | String? | 初始打开的目录路径 |
| confirmButtonText | String? | 确认按钮的文本 |
7.2 XTypeGroup 属性
| 属性 | 类型 | 说明 |
|---|---|---|
| label | String? | 分组标签 |
| extensions | List <String>? |
文件扩展名列表 |
| mimeTypes | List <String>? |
MIME 类型列表 |
| uniformTypeIdentifiers | List <String>? |
统一类型标识符列表 |
| webWildCards | List <String>? |
Web 通配符列表 |
7.3 XFile 属性和方法
| 属性/方法 | 返回值 | 说明 |
|---|---|---|
| name | String | 文件名 |
| path | String | 文件路径 |
| length() | Future <int> |
文件大小(字节) |
| mimeType | String? | MIME 类型 |
| readAsBytes() | Future <Uint8List> |
读取为字节数组 |
| readAsString() | Future <String> |
读取为字符串 |
| openRead() | Stream <Uint8List> |
读取为流 |
| saveTo(String path) | Future <void> |
保存到指定路径 |
八、常见问题与解决方案
8.1 用户取消选择
问题描述:用户点击取消后如何处理。
解决方案:
final XFile? file = await FileSelectorPlatform.instance.openFile(...);
if (file == null) {
// 用户取消了选择
return;
}
// 处理选中的文件
8.2 文件类型过滤不生效
问题描述:设置的文件类型过滤没有生效。
解决方案:
确保 XTypeGroup 配置正确,并且至少设置一种过滤方式:
// 正确示例
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png'],
);
// 跨平台兼容,设置多种过滤方式
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png'],
mimeTypes: <String>['image/jpeg', 'image/png'],
uniformTypeIdentifiers: <String>['public.image'],
);
8.3 读取大文件内存溢出
问题描述:读取大文件时内存溢出。
解决方案:
// 使用流式读取
final Stream<Uint8List> stream = file.openRead();
stream.listen(
(Uint8List chunk) {
// 分块处理数据
},
onError: (error) {
print('读取错误: $error');
},
onDone: () {
print('读取完成');
},
);
8.4 OpenHarmony 平台特殊配置
问题描述 1:在 OpenHarmony 平台上无法使用文件选择器。
解决方案:
- 设置平台实例:确保在
main.dart中设置了平台实例:
import 'package:file_selector_ohos/file_selector_ohos.dart';
void main() {
FileSelectorPlatform.instance = FileSelectorOhos();
runApp(const MyApp());
}
- 配置网络权限:在
ohos/entry/src/main/module.json5中添加完整的权限配置:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
- 添加权限说明:在
ohos/entry/src/main/resources/base/element/string.json中添加:
{
"name": "network_reason",
"value": "使用网络访问文件资源"
}
⚠️ 注意:权限配置必须包含
reason和usedScene字段,否则会导致应用安装失败(错误代码 9568289)。
九、最佳实践
9.1 正确处理用户取消
Future<void> _openFile() async {
final XFile? file = await FileSelectorPlatform.instance.openFile(...);
if (file == null) {
// 用户取消选择,优雅处理
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已取消选择')),
);
return;
}
// 处理选中的文件
}
9.2 文件类型过滤
// 为不同平台设置适当的过滤方式
List<XTypeGroup> getTypeGroups() {
final List<XTypeGroup> groups = <XTypeGroup>[];
// 所有平台都支持的扩展名过滤
groups.add(XTypeGroup(
label: 'Images',
extensions: <String>['jpg', 'png', 'gif'],
));
// macOS/iOS 的统一类型标识符
groups.add(XTypeGroup(
label: 'Images',
uniformTypeIdentifiers: <String>['public.image'],
));
// Web 的 MIME 类型
groups.add(XTypeGroup(
label: 'Images',
mimeTypes: <String>['image/jpeg', 'image/png'],
));
return groups;
}
9.3 显示文件信息
Future<void> _showFileInfo(XFile file) async {
final String fileName = file.name;
final String path = file.path;
final int length = await file.length();
final String? mimeType = file.mimeType;
final String sizeText = _formatFileSize(length);
print('文件名: $fileName');
print('路径: $path');
print('大小: $sizeText');
print('类型: $mimeType');
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
9.4 错误处理
Future<void> _openFile() async {
try {
final XFile? file = await FileSelectorPlatform.instance.openFile(...);
if (file == null) return;
final bytes = await file.readAsBytes();
// 处理文件内容
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('打开文件失败: $e')),
);
}
}
十、总结
本文详细介绍了 Flutter for OpenHarmony 中 file_selector 文件选择插件的使用方法,包括:
- 基础概念:file_selector 的特点、功能对比、文件类型过滤
- 平台适配:兼容性信息、权限配置
- 项目配置:依赖添加、平台实例设置、权限配置
- 核心 API:文件类型组、单文件选择、多文件选择、目录选择
- 实际应用:完整的文件选择应用示例
- 最佳实践:用户取消处理、文件类型过滤、文件信息显示、错误处理
file_selector 是一个功能完整的文件选择插件,适合各种需要文件选择功能的应用。
OpenHarmony 平台注意事项:
- 在
main.dart中设置平台实例(FileSelectorPlatform.instance = FileSelectorOhos()) - 必须配置完整的网络权限(
ohos.permission.INTERNET),包含reason和usedScene字段 - **目录选择功能(
getDirectoryPath)在原始版本中未实现 - 不支持选择保存位置(
getSaveLocation)
十一、参考资料
📌 提示:本文基于 Flutter 3.27.5 和 file_selector@1.0.3 编写,不同版本可能略有差异。
更多推荐



所有评论(0)