Flutter for OpenHarmony 实战:file_selector — 原生文件选择指南
前言
在移动应用开发中,让用户选择文件(如图片、文档、视频)是一个非常高频的需求。虽然 image_picker 可以处理图片和视频,但当我们需要选择任意类型的文件(如 PDF、JSON、ZIP)时,就需要更通用的解决方案。
file_selector 是 Flutter 官方提供的插件,旨在提供跨平台(iOS, Android, Web, Desktop, OpenHarmony)的统一文件选择能力。在 OpenHarmony 上,它对接了系统的 FilePicker 接口,能够拉起原生的文件选择器,用户体验非常流畅。
本文将介绍如何在 OpenHarmony 项目中集成 file_selector,实现单文件选择、多文件选择以及保存文件(另存为)的功能。
一、核心功能
file_selector 的 API 设计非常简洁,主要包含以下几个核心方法:
- openFile: 选择单个文件。
- openFiles: 选择多个文件。
- path_provider: 保存文件
- getSaveLocation: 获取文件保存路径(即“另存为”对话框)。
二、安装与配置
2.1 添加依赖
在 pubspec.yaml 中添加:
dependencies:
flutter:
sdk: flutter
# 请务必检查 pub.dev 或 OpenHarmony SIG 仓库获取适配 OHOS 的版本
file_selector: ^1.0.0
file_selector_ohos: any
path_provider_ohos: any
path_provider: ^2.1.5
dependency_overrides:
# File Selector 全套覆盖
file_selector:
git:
url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
path: "packages/file_selector/file_selector"
file_selector_platform_interface:
git:
url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
path: "packages/file_selector/file_selector_platform_interface"
file_selector_ohos:
git:
url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
path: "packages/file_selector/file_selector_ohos"
# Path Provider Adapter
path_provider_ohos:
git:
url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
path: "packages/path_provider/path_provider_ohos"

2.2 OpenHarmony 权限配置
使用系统的 Picker 接口选择文件通常是一个“用户授权”的过程(用户主动选择文件即视为授权),因此通常不需要像 Android 那样申请 READ_EXTERNAL_STORAGE 权限。OpenHarmony 的沙箱机制会自动授权应用读取用户选中的那个文件。
但是,如果你需要访问特定公共目录,可以按需检查 module.json5 中的权限配置,一般来说,基础的文件选择无需额外配置。
三、代码实现
3.1 选择单个文件
我们可以指定 XTypeGroup 来过滤文件类型,例如只允许选择文本文件或图片。
import 'package:flutter/material.dart';
import 'package:file_selector/file_selector.dart';
class FileSelectorPage extends StatelessWidget {
const FileSelectorPage({super.key});
Future<void> _pickSingleFile(BuildContext context) async {
// 定义文件类型过滤器
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png'],
);
// 打开选择器
final XFile? file = await openFile(
acceptedTypeGroups: <XTypeGroup>[typeGroup],
);
if (file != null) {
// 获取文件名和路径
print('文件名: ${file.name}');
print('路径: ${file.path}');
// 读取文件内容
final int size = await file.length();
print('文件大小: $size 字节');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已选择: ${file.name}')),
);
} else {
// 用户取消了选择
print('用户取消操作');
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('文件选择演示')),
body: Center(
child: ElevatedButton(
onPressed: () => _pickSingleFile(context),
child: const Text('选择一张图片'),
),
),
);
}
}

3.2 选择多个文件
使用 openFiles 方法即可,返回的是 List<XFile>。
Future<void> _pickMultipleFiles() async {
final List<XFile> files = await openFiles(
acceptedTypeGroups: <XTypeGroup>[
const XTypeGroup(label: 'All Files'), // 允许所有类型
],
);
print('选择了 ${files.length} 个文件');
for (var file in files) {
print('路径: ${file.path}');
}
}

3.3 保存文件(另存为)
在 OpenHarmony 上,这会拉起系统的“保存文件”面板,让用户选择保存的位置和文件名。
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_selector/file_selector.dart';
import 'package:path_provider/path_provider.dart';
class SaveFilePage extends StatelessWidget {
const SaveFilePage({super.key});
Future<void> _saveFile(BuildContext context) async {
final String fileName =
'my_export_data_${DateTime.now().millisecondsSinceEpoch}.txt';
try {
debugPrint('准备保存文件…');
// 1. 获取应用私有文档目录 (Sandboxed)
// 由于 file_selector_ohos 暂未实现 getSaveLocation,
// 我们推荐使用标准路径保存数据
final Directory directory = await getApplicationDocumentsDirectory();
final String path = '${directory.path}/$fileName';
debugPrint('目标路径: $path');
// 2. 写入文件
final File file = File(path);
await file.writeAsString(
'Hello OpenHarmony from Flutter! \\nSaved at ${DateTime.now()}');
debugPrint('文件写入成功');
if (context.mounted) {
// 3. 提示用户
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('保存成功'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('文件已保存至应用沙箱目录:'),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[200],
child: Text(path, style: const TextStyle(fontSize: 12)),
),
const SizedBox(height: 16),
const Text(
'注:由于 file_selector 暂未适配 OpenHarmony "另存为" 接口,此处演示使用 path_provider 保存到应用私有目录。',
style: TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('好的'),
),
],
),
);
}
} catch (e) {
debugPrint('保存出错: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败: $e')),
);
}
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('3.3 保存文件 (替代方案)')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'注:OpenHarmony 暂不支持 file_selector 的 "另存为" (getSaveLocation)。\\n'
'本页演示使用 path_provider 保存文件到应用沙箱。',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
ElevatedButton(
onPressed: () => _saveFile(context),
child: const Text('保存 text 文件到沙箱'),
),
],
),
),
);
}
}

四、OpenHarmony 平台适配细节
4.1 路径与沙箱
OpenHarmony 采用严格的沙箱机制。当用户通过 Picker 选择文件后,系统通常会授予应用对该文件的临时读写权限。file_selector 返回的 path 通常是一个可以直接供 Dart File 对象读取的路径。
4.2 MIME Type 过滤
XTypeGroup 支持 extensions(扩展名)、mimeTypes(MIME类型)和 macUTIs(iOS专用)。在 OpenHarmony 上,建议优先使用 extensions,兼容性最好。
// 推荐做法:使用扩展名
const XTypeGroup(extensions: ['pdf', 'doc']);
// 可能存在兼容性差异:使用 MIME
// const XTypeGroup(mimeTypes: ['application/pdf']);
4.3 UI 差异
file_selector 在 OpenHarmony 上调用的是系统级的 UI(FilePicker)。这意味着 UI 样式(如列表视图、网格视图、排序方式)是由系统决定的,Flutter 无法修改其外观,只能控制可选的文件类型。
五、完整示例代码
下面是一个综合示例,包含单选、多选和保存功能。
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_selector/file_selector.dart';
import 'package:path_provider/path_provider.dart';
class FileSelectorPage extends StatefulWidget {
const FileSelectorPage({super.key});
State<FileSelectorPage> createState() => _FileSelectorPageState();
}
class _FileSelectorPageState extends State<FileSelectorPage> {
String _statusText = '请选择操作';
List<XFile> _selectedFiles = [];
// 选择图片
Future<void> _pickImage() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'png', 'gif'],
);
// 捕获异常,防止在不支持的平台上崩溃
try {
final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]);
if (file != null) {
setState(() {
_statusText = '已选择: ${file.name}';
_selectedFiles = [file];
});
} else {
// 用户取消
}
} catch (e) {
setState(() {
_statusText = '错误: $e';
});
}
}
// 选择多个文档
Future<void> _pickDocs() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'documents',
extensions: <String>['pdf', 'txt', 'json'],
);
try {
final List<XFile> files =
await openFiles(acceptedTypeGroups: [typeGroup]);
if (files.isNotEmpty) {
setState(() {
_statusText = '已选择 ${files.length} 个文件';
_selectedFiles = files;
});
}
} catch (e) {
setState(() {
_statusText = '错误: $e';
});
}
}
// 另存为 (模拟导出功能)
// 注: 由于 file_selector_ohos 暂未实现 getSaveLocation,这里使用 path_provider 替代
Future<void> _saveText() async {
try {
final Directory directory = await getApplicationDocumentsDirectory();
final String fileName =
'example_${DateTime.now().millisecondsSinceEpoch}.txt';
final String path = '${directory.path}/$fileName';
// 写入数据
final file = File(path);
await file.writeAsString(
'这是通过 Flutter file_selector 页面演示保存的文件内容。\\nTime: ${DateTime.now()}');
if (!mounted) return;
setState(() {
_statusText = '文件已保存至沙箱: $path';
});
// 弹出详细信息的 Dialog
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('保存成功 (path_provider)'),
content: SingleChildScrollView(
child: Text('文件已写入应用私有目录:\\n\\n$path\\n\\n(OpenHarmony 暂不支持原生“另存为”面板)'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx), child: const Text('OK'))
],
),
);
} catch (e) {
setState(() {
_statusText = '保存失败: $e';
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('File Selector OHOS')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(_statusText, style: const TextStyle(fontSize: 16)),
),
const SizedBox(height: 20),
// 选中文件列表区域
Expanded(
child: _selectedFiles.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.folder_open,
size: 64, color: Colors.indigo[100]),
const SizedBox(height: 16),
const Text('暂无选中文件',
style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
itemCount: _selectedFiles.length,
itemBuilder: (ctx, index) {
final file = _selectedFiles[index];
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
_getFileIcon(file.name),
color: Colors.indigo,
),
title: Text(file.name),
subtitle: Text(file.path,
maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: Text(
_formatSize(
index), // 仅为演示,实际需异步获取 await file.length()
style: const TextStyle(
fontSize: 12, color: Colors.grey),
),
),
);
},
),
),
const Divider(),
// 操作按钮区域
Wrap(
spacing: 16,
runSpacing: 16,
alignment: WrapAlignment.center,
children: [
_ActionButton(
icon: Icons.image,
label: '选图片',
color: Colors.purple,
onPressed: _pickImage,
),
_ActionButton(
icon: Icons.library_books,
label: '选文档(多选)',
color: Colors.orange,
onPressed: _pickDocs,
),
_ActionButton(
icon: Icons.save,
label: '另存为',
color: Colors.blue,
onPressed: _saveText,
),
],
),
const SizedBox(height: 20),
],
),
),
);
}
IconData _getFileIcon(String name) {
final lowerName = name.toLowerCase();
if (lowerName.endsWith('.jpg') || lowerName.endsWith('.png'))
return Icons.image;
if (lowerName.endsWith('.pdf')) return Icons.picture_as_pdf;
if (lowerName.endsWith('.txt')) return Icons.description;
if (lowerName.endsWith('.json')) return Icons.code;
return Icons.insert_drive_file;
}
String _formatSize(int index) {
// 真实场景下 XFile.length() 是 Future,这里为了 UI 简洁未调用
return 'Tap to read';
}
}
class _ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
final VoidCallback onPressed;
const _ActionButton({
required this.icon,
required this.label,
required this.color,
required this.onPressed,
});
Widget build(BuildContext context) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
);
}
}

六、总结
file_selector 为 OpenHarmony 应用提供了标准化的文件交互能力。相比于直接调用原生 Channel,使用此插件能保持代码的跨平台一致性。
在处理大文件读取时,建议配合 Dart 的 Stream 或 Isolate 来避免阻塞 UI 线程。
🌐 欢迎加入开源鸿蒙跨平台社区:开源鸿蒙跨平台开发者社区
网硕互联帮助中心




评论前必须登录!
注册