欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、file_selector 库简介
file_selector 是 Flutter 官方维护的文件选择插件,提供了跨平台的文件选择对话框功能。无论是导入文档、上传图片、选择配置文件,还是批量处理文件,file_selector 都能提供原生体验的文件选择界面。
📋 file_selector 核心特点
| 特点 | 说明 |
|---|---|
| 单文件选择 | 支持选择单个文件,可指定文件类型过滤 |
| 多文件选择 | 支持一次选择多个文件 |
| 目录选择 | 支持选择目录路径(源码中没有实现) |
| 文件类型过滤 | 支持扩展名、MIME 类型等多种过滤方式 |
| 初始目录设置 | 可设置文件选择器的初始打开目录 |
| 自定义按钮文本 | 支持自定义确认按钮文本 |
| 跨平台兼容 | 支持 Android、iOS、Web、Windows、Linux、macOS、OpenHarmony |
平台功能支持对比
| 功能 | Android | iOS | Web | OpenHarmony |
|---|---|---|---|---|
| 选择单个文件 | ✔️ | ✔️ | ✔️ | ✔️ |
| 选择多个文件 | ✔️ | ✔️ | ✔️ | ✔️ |
| 选择目录 | ✔️ | ❌ | ❌ | ❌ |
| 选择保存位置 | ❌ | ❌ | ❌ | ❌ |
| 文件类型过滤 | ✔️ | ✔️ | ✔️ | ✔️ |
| 初始目录设置 | ✔️ | ✔️ | ❌ | ✔️ |
| 自定义按钮文本 | ✔️ | ✔️ | ❌ | ✔️ |
文件类型过滤支持
| 过滤类型 | 说明 | OpenHarmony 支持 |
|---|---|---|
| extensions | 文件扩展名(如 jpg、png) | ✔️ |
| mimeTypes | MIME 类型(如 image/jpeg) | ✔️ |
| uniformTypeIdentifiers | 统一类型标识符 | ✔️ |
| webWildCards | Web 通配符(如 image/*) | ✔️ |
使用场景
- 文档导入与导出
- 图片/视频上传
- 配置文件加载
- 批量文件处理
- 文件管理器应用
二、OpenHarmony 适配版本
2.1 环境说明
| 组件 | 版本 |
|---|---|
| Flutter | 3.27.5 |
| HarmonyOS | 6.0 |
| file_selector | 1.0.3 (OpenHarmony 适配版本) |
2.2 引入方式
在 pubspec.yaml 文件中添加以下依赖配置:
yaml
dependencies:
flutter:
sdk: flutter
# file_selector OpenHarmony 适配版本
file_selector:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages
path: packages/file_selector/file_selector
2.3 获取依赖
配置完成后,在项目根目录执行:
bash
flutter pub get
2.4 权限配置
file_selector 使用系统原生的文件选择器,会自动处理文件访问权限,因此不需要申请媒体读写权限。
如果应用需要访问网络资源(例如加载远程文件),则需要配置网络权限:
打开 ohos/entry/src/main/module.json5,在 requestPermissions 中添加:
json
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
在 ohos/entry/src/main/resources/base/element/string.json 中添加权限说明:
json
{
"name": "network_reason",
"value": "使用网络访问文件资源"
}
三、核心 API 讲解
3.1 顶层函数 API
file_selector 提供了简洁的顶层函数 API,无需实例化类即可直接调用。
dart
import 'package:file_selector/file_selector.dart';
3.2 XTypeGroup 文件类型组
XTypeGroup 用于定义可选择的文件类型,是文件过滤的核心配置。
dart
const XTypeGroup({
String? label, // 可选:分组标签
List<String> extensions = const [], // 可选:文件扩展名列表
List<String> mimeTypes = const [], // 可选:MIME 类型列表
List<String> uniformTypeIdentifiers = const [], // 可选:统一类型标识符
List<String> webWildCards = const [], // 可选:Web 通配符
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| label | String? | 否 | null | 分组标签,用于在文件选择器中显示类型名称 |
| extensions | List<String> |
否 | [] | 文件扩展名列表,如 ['jpg', 'png'] |
| mimeTypes | List<String> |
否 | [] | MIME 类型列表,如 ['image/jpeg', 'image/png'] |
| uniformTypeIdentifiers | List<String> |
否 | [] | 统一类型标识符,如 'public.image' |
| webWildCards | List<String> |
否 | [] | Web 通配符,如 'image/*' |
使用示例:
dart
// 定义图片类型组
const XTypeGroup imageTypeGroup = XTypeGroup(
label: '图片',
extensions: ['jpg', 'jpeg', 'png', 'gif'],
mimeTypes: ['image/jpeg', 'image/png', 'image/gif'],
);
// 定义文档类型组
const XTypeGroup documentTypeGroup = XTypeGroup(
label: '文档',
extensions: ['pdf', 'doc', 'docx', 'txt'],
mimeTypes: ['application/pdf', 'application/msword'],
);
// 定义所有文件类型
const XTypeGroup allFilesTypeGroup = XTypeGroup(
label: '所有文件',
extensions: [], // 空列表表示不限制
);
3.3 openFile - 选择单个文件
dart
Future<XFile?> openFile({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
String? initialDirectory,
String? confirmButtonText,
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| acceptedTypeGroups | List<XTypeGroup> |
否 | [] | 可选择的文件类型组列表,空列表表示不限制 |
| initialDirectory | String? | 否 | null | 初始打开的目录路径 |
| confirmButtonText | String? | 否 | null | 确认按钮文本,null 时使用系统默认文本 |
返回值: Future<XFile?> - 返回选中的文件对象,用户取消时返回 null
使用示例:
dart
// 选择单个图片文件
final XFile? file = await openFile(
acceptedTypeGroups: [imageTypeGroup],
confirmButtonText: '选择图片',
);
// 选择任意文件
final XFile? file = await openFile(
confirmButtonText: '选择文件',
);
3.4 openFiles - 选择多个文件
dart
Future<List<XFile>> openFiles({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
String? initialDirectory,
String? confirmButtonText,
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| acceptedTypeGroups | List<XTypeGroup> |
否 | [] | 可选择的文件类型组列表 |
| initialDirectory | String? | 否 | null | 初始打开的目录路径 |
| confirmButtonText | String? | 否 | null | 确认按钮文本 |
返回值: Future<List<XFile>> - 返回选中的文件列表,用户取消时返回空列表
使用示例:
dart
// 选择多个图片文件
final List<XFile> files = await openFiles(
acceptedTypeGroups: [imageTypeGroup],
confirmButtonText: '选择图片',
);
3.5 getDirectoryPath - 选择目录(但是我看到源码中没有实现这个)
dart
Future<String?> getDirectoryPath({
String? initialDirectory,
String? confirmButtonText,
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| initialDirectory | String? | 否 | null | 初始打开的目录路径 |
| confirmButtonText | String? | 否 | null | 确认按钮文本 |
返回值: Future<String?> - 返回选中的目录路径,用户取消时返回 null
注意: OpenHarmony 平台不支持 getSaveLocation(选择保存位置),仅支持选择目录。
3.6 getDirectoryPaths - 选择多个目录
dart
Future<List<String?>> getDirectoryPaths({
String? initialDirectory,
String? confirmButtonText,
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| initialDirectory | String? | 否 | null | 初始打开的目录路径 |
| confirmButtonText | String? | 否 | null | 确认按钮文本 |
返回值: Future<List<String?>> - 返回选中的目录路径列表,用户取消时返回空列表
3.7 getSaveLocation - 选择保存位置
dart
Future<FileSaveLocation?> getSaveLocation({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
String? initialDirectory,
String? suggestedName,
String? confirmButtonText,
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| acceptedTypeGroups | List<XTypeGroup> |
否 | [] | 文件类型组列表 |
| initialDirectory | String? | 否 | null | 初始目录 |
| suggestedName | String? | 否 | null | 建议的文件名 |
| confirmButtonText | String? | 否 | null | 确认按钮文本 |
返回值: Future<FileSaveLocation?> - 返回保存位置信息,用户取消时返回 null
注意: OpenHarmony 平台不支持此方法,调用会返回 null 或抛出异常。
3.8 XFile 文件对象
XFile 是跨平台文件抽象类,提供了标准的文件操作接口。
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
| name | String | 文件名(包含扩展名) |
| path | String? | 文件路径(部分平台可能为 null) |
| length | Future<int> |
文件大小(字节) |
| mimeType | String? | 文件的 MIME 类型 |
常用方法
| 方法 | 返回值 | 说明 |
|---|---|---|
| readAsBytes() | Future<Uint8List> |
读取文件内容为字节数组 |
| readAsString() | Future<String> |
读取文件内容为字符串 |
| openRead() | Stream<Uint8List> |
以流的方式读取文件 |
| saveTo(String path) | Future<void> |
将文件保存到指定路径 |
四、完整使用示例

以下是一个完整的文件选择应用,整合了单文件选择、多文件选择、目录选择等功能:
dart
import 'dart:io';
import 'dart:typed_data';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const FileSelectorApp());
}
class FileSelectorApp extends StatelessWidget {
const FileSelectorApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'file_selector 示例',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF10B981)),
useMaterial3: true,
),
home: const FileSelectorHomePage(),
);
}
}
class FileSelectorHomePage extends StatefulWidget {
const FileSelectorHomePage({super.key});
@override
State<FileSelectorHomePage> createState() => _FileSelectorHomePageState();
}
class _FileSelectorHomePageState extends State<FileSelectorHomePage> {
XFile? _selectedFile;
List<XFile> _selectedFiles = [];
String? _selectedDirectory;
String? _selectedFilePath;
Uint8List? _imageBytes;
String? _textContent;
// 定义文件类型组
static const XTypeGroup imageTypeGroup = XTypeGroup(
label: '图片',
extensions: ['jpg', 'jpeg', 'png', 'gif'],
mimeTypes: ['image/jpeg', 'image/png', 'image/gif'],
);
static const XTypeGroup textTypeGroup = XTypeGroup(
label: '文本文件',
extensions: ['txt', 'json', 'md', 'csv'],
mimeTypes: ['text/plain', 'application/json'],
);
static const XTypeGroup documentTypeGroup = XTypeGroup(
label: '文档',
extensions: ['pdf', 'doc', 'docx'],
mimeTypes: ['application/pdf', 'application/msword'],
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('file_selector 文件选择'),
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSectionTitle('单文件选择'),
_buildButton(
icon: Icons.image,
label: '选择图片',
onTap: _selectImage,
),
_buildButton(
icon: Icons.text_snippet,
label: '选择文本文件',
onTap: _selectTextFile,
),
_buildButton(
icon: Icons.insert_drive_file,
label: '选择任意文件',
onTap: _selectAnyFile,
),
if (_selectedFile != null) _buildFilePreview(),
const SizedBox(height: 24),
_buildSectionTitle('多文件选择'),
_buildButton(
icon: Icons.photo_library,
label: '选择多张图片',
onTap: _selectMultipleImages,
),
_buildButton(
icon: Icons.folder_open,
label: '选择多个文件',
onTap: _selectMultipleFiles,
),
if (_selectedFiles.isNotEmpty) _buildMultipleFilesPreview(),
const SizedBox(height: 24),
_buildSectionTitle('目录选择'),
_buildButton(
icon: Icons.folder,
label: '选择目录',
onTap: _selectDirectory,
),
if (_selectedDirectory != null) _buildDirectoryPreview(),
const SizedBox(height: 32),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF10B981),
),
),
);
}
Widget _buildButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ElevatedButton.icon(
onPressed: onTap,
icon: Icon(icon, color: Colors.white),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
);
}
Widget _buildFilePreview() {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选中的文件: ${_selectedFile!.name}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
if (_imageBytes != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(
_imageBytes!,
height: 200,
width: double.infinity,
fit: BoxFit.contain,
),
),
if (_textContent != null)
Container(
constraints: const BoxConstraints(maxHeight: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(
_textContent!,
style: const TextStyle(fontFamily: 'monospace'),
),
),
),
const SizedBox(height: 8),
Text('路径: ${_selectedFile!.path}'),
FutureBuilder<int>(
future: _selectedFile!.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('大小: ${_formatFileSize(snapshot.data!)}');
}
return const SizedBox.shrink();
},
),
],
),
),
);
}
Widget _buildMultipleFilesPreview() {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选中的文件: ${_selectedFiles.length} 个',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
SizedBox(
height: 150,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _selectedFiles.length,
itemBuilder: (context, index) {
final file = _selectedFiles[index];
return Padding(
padding: const EdgeInsets.only(right: 8),
child: Container(
width: 150,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getFileIcon(file.name),
size: 48,
color: const Color(0xFF10B981),
),
const SizedBox(height: 8),
Text(
file.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
],
),
),
);
},
),
),
],
),
),
);
}
Widget _buildDirectoryPreview() {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选中的目录',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.folder, color: Color(0xFF10B981), size: 32),
const SizedBox(width: 12),
Expanded(
child: Text(
_selectedDirectory!,
style: const TextStyle(fontFamily: 'monospace'),
),
),
],
),
),
],
),
),
);
}
IconData _getFileIcon(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
switch (extension) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return Icons.image;
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'txt':
case 'md':
return Icons.text_snippet;
case 'json':
case 'csv':
return Icons.data_usage;
default:
return Icons.insert_drive_file;
}
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
Future<void> _selectImage() async {
final XFile? file = await openFile(
acceptedTypeGroups: [imageTypeGroup],
confirmButtonText: '选择图片',
);
if (file != null && mounted) {
final bytes = await file.readAsBytes();
setState(() {
_selectedFile = file;
_imageBytes = bytes;
_textContent = null;
});
}
}
Future<void> _selectTextFile() async {
final XFile? file = await openFile(
acceptedTypeGroups: [textTypeGroup],
confirmButtonText: '选择文件',
);
if (file != null && mounted) {
final content = await file.readAsString();
setState(() {
_selectedFile = file;
_textContent = content;
_imageBytes = null;
});
}
}
Future<void> _selectAnyFile() async {
final XFile? file = await openFile(
confirmButtonText: '选择文件',
);
if (file != null && mounted) {
final bytes = await file.readAsBytes();
final isImage = ['jpg', 'jpeg', 'png', 'gif']
.contains(file.name.split('.').last.toLowerCase());
final isText = ['txt', 'json', 'md', 'csv']
.contains(file.name.split('.').last.toLowerCase());
setState(() {
_selectedFile = file;
if (isImage) {
_imageBytes = bytes;
_textContent = null;
} else if (isText) {
_textContent = String.fromCharCodes(bytes);
_imageBytes = null;
} else {
_imageBytes = null;
_textContent = null;
}
});
}
}
Future<void> _selectMultipleImages() async {
final List<XFile> files = await openFiles(
acceptedTypeGroups: [imageTypeGroup],
confirmButtonText: '选择图片',
);
if (files.isNotEmpty && mounted) {
setState(() {
_selectedFiles = files;
});
}
}
Future<void> _selectMultipleFiles() async {
final List<XFile> files = await openFiles(
confirmButtonText: '选择文件',
);
if (files.isNotEmpty && mounted) {
setState(() {
_selectedFiles = files;
});
}
}
Future<void> _selectDirectory() async {
final String? directoryPath = await getDirectoryPath(
confirmButtonText: '选择目录',
);
if (directoryPath != null && mounted) {
setState(() {
_selectedDirectory = directoryPath;
});
}
}
}
五、适配要点
5.1 OpenHarmony 平台特性
-
权限处理
- file_selector 使用系统原生的文件选择器(PhotoViewPicker、DocumentViewPicker)
- 选择器会自动处理文件访问权限,无需额外申请媒体权限
- 仅在需要访问网络资源时才需要配置网络权限
-
文件路径处理
- OpenHarmony 平台返回的文件路径为沙箱路径
- 路径仅在应用生命周期内有效,不应跨会话保存
- 如需持久化存储,应使用
saveTo方法复制文件到持久化目录
-
文件类型过滤
- OpenHarmony 平台支持 extensions 和 mimeTypes 过滤
- uniformTypeIdentifiers 会被映射到系统对应的类型标识符
- 空列表表示不限制文件类型
-
目录选择
- OpenHarmony 平台支持目录选择功能
- 返回的目录路径可用于后续文件操作
- 不支持多目录选择(getDirectoryPaths 可能返回空或单元素列表)
5.2 与 Android/iOS 的差异
| 差异点 | Android/iOS | OpenHarmony |
|---|---|---|
| 权限申请 | 运行时动态申请 | 声明式权限,选择器自动处理 |
| 文件路径 | 持久化路径 | 沙箱临时路径 |
| 保存位置选择 | 部分平台支持 | 不支持 |
| 文件类型过滤 | 完整支持 | 支持,但映射方式不同 |
| 初始目录 | 支持 | 支持,但可能被系统忽略 |
| 自定义按钮文本 | 支持 | 支持 |
5.3 注意事项
-
不要保存文件路径
XFile的路径仅在应用会话内有效- 跨会话使用应复制文件到持久化目录
-
错误处理
- 所有方法都可能抛出异常
- 建议使用 try-catch 包裹调用代码
-
文件类型组配置
- 至少提供一种过滤方式(extensions 或 mimeTypes)
- 多个类型组会在选择器中显示为可切换的过滤选项
-
大文件处理
- 使用
readAsBytes()读取大文件时注意内存占用 - 建议使用
openRead()流式读取大文件
- 使用
六、常见问题
Q1: 选择文件后无法读取内容?
原因: 文件路径为沙箱临时路径,可能已被清理。
解决方案:
dart
// 错误做法:保存路径后使用
String path = file.path!; // 跨会话可能失效
// 正确做法:立即读取内容或复制文件
final bytes = await file.readAsBytes();
// 或
final newPath = '${persistentDir.path}/${file.name}';
await file.saveTo(newPath);
Q2: 文件类型过滤不生效?
原因: 过滤条件配置不正确或平台不支持。
解决方案:
dart
// 确保至少提供一种过滤方式
const XTypeGroup typeGroup = XTypeGroup(
label: '图片',
extensions: ['jpg', 'png'], // 提供扩展名
mimeTypes: ['image/jpeg', 'image/png'], // 提供 MIME 类型
);
Q3: 选择目录返回 null?
原因: 用户取消了选择或平台不支持。
解决方案:
dart
final String? directoryPath = await getDirectoryPath();
if (directoryPath == null) {
// 用户取消或平台不支持
return;
}
// 处理选中的目录
Q4: 如何判断文件类型?
解决方案:
dart
String getFileType(XFile file) {
final extension = file.name.split('.').last.toLowerCase();
switch (extension) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return '图片';
case 'pdf':
return 'PDF文档';
case 'txt':
case 'md':
return '文本文件';
case 'json':
return 'JSON文件';
default:
return '其他文件';
}
}
Q5: 如何处理大文件?
解决方案:
dart
// 使用流式读取,避免内存溢出
final Stream<Uint8List> stream = file.openRead();
await for (final chunk in stream) {
// 处理数据块
processChunk(chunk);
}
// 或使用分块读取
final int fileSize = await file.length();
const int chunkSize = 1024 * 1024; // 1MB
for (int offset = 0; offset < fileSize; offset += chunkSize) {
final chunk = await readChunk(file, offset, chunkSize);
processChunk(chunk);
}
Q6: 如何在列表中显示文件图标?
解决方案:
dart
IconData getFileIcon(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
switch (extension) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return Icons.image;
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'txt':
case 'md':
return Icons.text_snippet;
case 'json':
case 'csv':
return Icons.data_usage;
default:
return Icons.insert_drive_file;
}
}
Q7: 应用重启后如何恢复之前选择的文件?
解决方案:
dart
// 保存时复制文件到持久化目录
Future<String> saveFilePermanently(XFile file) async {
final directory = await getApplicationDocumentsDirectory();
final newPath = '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_${file.name}';
await file.saveTo(newPath);
return newPath;
}
// 使用时从持久化目录加载
Image.file(File(savedPath));
七、总结
file_selector 是 Flutter 生态中最常用的文件选择插件,在 OpenHarmony 平台的适配已经非常成熟。通过本文的介绍,你应该已经掌握了:
- file_selector 的核心 API 和使用方法
- XTypeGroup 文件类型过滤的配置方式
- 完整的文件选择应用实现
- OpenHarmony 平台的权限配置和适配要点
- 常见问题和解决方案
在实际开发中,建议根据具体需求合理配置文件类型过滤,并注意文件路径的临时性特点。对于需要持久化存储的场景,务必使用 saveTo 方法将文件复制到应用的持久化目录中。
💡 提示: 更多 OpenHarmony 适配的 Flutter 三方库信息,请访问 开源鸿蒙跨平台开发者社区 获取最新资源和技术支持。