欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
本文基于flutter3.27.5开发
一、file_selector 库概述
文件选择是移动应用和桌面应用中非常常见的功能,无论是上传文档、导入配置文件、选择媒体资源还是批量处理文件,都需要使用到文件选择器。在 Flutter for OpenHarmony 应用开发中,file_selector 是一个功能强大的文件选择插件,提供了完整的文件选择和管理功能,支持多种文件类型过滤和自定义配置。
📋 file_selector 库特点
file_selector 库基于 Flutter 平台接口实现,提供了以下核心特性:
单文件选择 :支持选择单个文件,返回 XFile 对象,包含文件路径、名称、大小等信息。适用于头像上传、文档导入等场景。
多文件选择 :支持一次选择多个文件,返回 List<XFile> 对象列表。适用于批量上传、多文件处理等场景。
目录选择:经过源码分析,该功能未实现
文件类型过滤:支持通过扩展名、MIME 类型、统一类型标识符等多种方式过滤文件类型,帮助用户快速定位目标文件。
初始目录设置:支持设置文件选择器的初始打开目录,提升用户体验。
自定义按钮文本:支持自定义确认按钮的文本内容,适应不同的业务场景。
支持平台对比
| 平台 | 支持情况 | 说明 |
|---|---|---|
| Android | ✅ 完全支持 | 支持 Android SDK 19+ |
| iOS | ✅ 完全支持 | 支持 iOS 11+ |
| Web | ✅ 完全支持 | 支持文件选择 |
| Windows | ✅ 完全支持 | 支持文件选择和保存 |
| macOS | ✅ 完全支持 | 支持文件选择和保存 |
| Linux | ✅ 完全支持 | 支持文件选择和保存 |
| OpenHarmony | ✅ 完全支持 | 专门适配,支持 API 12+ |
功能支持对比
| 功能 | Android | iOS | Web | OpenHarmony |
|---|---|---|---|---|
| 选择单个文件 | ✅ | ✅ | ✅ | ✅ |
| 选择多个文件 | ✅ | ✅ | ✅ | ✅ |
| 选择保存位置 | ❌ | ❌ | ❌ | ❌ |
| 选择目录 | ✅† | ❌ | ❌ | ❌ |
| 文件类型过滤 | ✅ | ✅ | ✅ | ✅ |
† Android SDK 21 (Lollipop) 及以上版本支持选择目录。
文件类型过滤支持
| 过滤类型 | 说明 | Android | iOS | OpenHarmony |
|---|---|---|---|---|
| extensions | 文件扩展名(如 jpg、png) | ✅ | ✅ | |
| mimeTypes | MIME 类型(如 image/jpeg) | ✅ | ✅ | |
| uniformTypeIdentifiers | 统一类型标识符(iOS/macOS) | ✅ | ✅ | |
| webWildCards | Web 通配符(如 image/*) | ✅ |
💡 使用场景:文档上传、图片选择、配置文件导入、批量文件处理、文件管理等。
二、安装与配置
2.1 添加依赖
在项目的 pubspec.yaml 文件中添加 file_selector 依赖:
yaml
dependencies:
file_selector:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages.git
path: packages/file_selector/file_selector
然后执行以下命令获取依赖:
bash
flutter pub get
2.2 权限配置
file_selector 在 OpenHarmony 平台上使用系统原生的文件选择器(PhotoViewPicker、DocumentViewPicker),这些选择器会自动处理文件访问权限,因此不需要申请媒体读写权限。
如果应用需要访问网络资源(例如加载远程文件),则需要配置网络权限:
在 entry 目录下的 module.json5 中添加权限
打开 ohos/entry/src/main/module.json5,在 requestPermissions 数组中添加:
json
"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 数组中添加:
json
{
"string": [
{
"name": "network_reason",
"value": "使用网络访问文件资源"
}
]
}
三、核心 API 详解
3.1 openFile - 选择单个文件
openFile 方法用于打开文件选择对话框,选择单个文件。
方法签名
dart
Future<XFile?> openFile({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
String? initialDirectory,
String? confirmButtonText,
})
参数详解
acceptedTypeGroups 是可选参数,指定可以在对话框中选择的文件类型组列表。通过设置此参数,可以过滤显示特定类型的文件,帮助用户快速定位目标文件。如果不设置,则显示所有类型的文件。
initialDirectory 是可选参数,指定对话框打开时显示的初始目录的完整路径。如果不设置,平台会选择一个默认的初始位置。
confirmButtonText 是可选参数,指定对话框确认按钮上的文本。如果不设置,使用操作系统的默认标签(如"打开")。
返回值
返回 Future<XFile?>,其中 XFile 是跨平台文件抽象类。如果用户取消选择,返回 null。
基本用法示例
dart
import 'package:file_selector/file_selector.dart';
class FileSelectorDemo extends StatefulWidget {
@override
_FileSelectorDemoState createState() => _FileSelectorDemoState();
}
class _FileSelectorDemoState extends State<FileSelectorDemo> {
XFile? _selectedFile;
Future<void> _openFile() async {
const XTypeGroup typeGroup = XTypeGroup(
label: '图片',
extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
);
final XFile? file = await openFile(
acceptedTypeGroups: [typeGroup],
);
if (file != null) {
setState(() {
_selectedFile = file;
});
print('选择的文件: ${file.path}');
print('文件名: ${file.name}');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('文件选择')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_selectedFile != null)
Text('已选择: ${_selectedFile!.name}')
else
Text('未选择文件'),
SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _openFile,
icon: Icon(Icons.folder_open),
label: Text('选择文件'),
),
],
),
),
);
}
}
3.2 openFiles - 选择多个文件
openFiles 方法用于打开文件选择对话框,选择多个文件。
方法签名
dart
Future<List<XFile>> openFiles({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
String? initialDirectory,
String? confirmButtonText,
})
参数详解
参数与 openFile 方法相同。
返回值
返回 Future<List<XFile>>,包含用户选择的所有文件。如果用户取消选择,返回空列表。
使用示例
dart
class MultiFileSelectorDemo extends StatefulWidget {
@override
_MultiFileSelectorDemoState createState() => _MultiFileSelectorDemoState();
}
class _MultiFileSelectorDemoState extends State<MultiFileSelectorDemo> {
List<XFile> _selectedFiles = [];
Future<void> _openFiles() async {
final List<XFile> files = await openFiles(
acceptedTypeGroups: [
XTypeGroup(
label: '文档',
extensions: ['pdf', 'doc', 'docx', 'txt'],
),
],
);
if (files.isNotEmpty) {
setState(() {
_selectedFiles = files;
});
for (XFile file in files) {
print('选择的文件: ${file.name}');
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('多文件选择')),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _selectedFiles.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text(_selectedFiles[index].name),
subtitle: Text(_selectedFiles[index].path),
);
},
),
),
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: _openFiles,
icon: Icon(Icons.folder_open),
label: Text('选择文件 (${_selectedFiles.length})'),
),
),
],
),
);
}
}
3.3 getDirectoryPath - 选择目录(虽然有API但是源码并未实现这个功能)
getDirectoryPath 方法用于打开目录选择对话框,选择一个目录。
方法签名
dart
Future<String?> getDirectoryPath({
String? initialDirectory,
String? confirmButtonText,
})
参数详解
initialDirectory 是可选参数,指定对话框打开时显示的初始目录的完整路径。
confirmButtonText 是可选参数,指定对话框确认按钮上的文本。
返回值
返回 Future<String?>,包含选择的目录路径。如果用户取消选择,返回 null。
3.4 XTypeGroup - 文件类型组
XTypeGroup 类用于定义文件类型组,可以设置多种过滤条件。
构造函数
dart
const XTypeGroup({
String? label,
List<String>? extensions,
List<String>? mimeTypes,
List<String>? uniformTypeIdentifiers,
List<String>? webWildCards,
})
属性详解
label 是可选参数,定义这个类型组的"名称"或引用,用于在文件选择器中显示给用户。例如"图片"、"文档"、"视频"等。
extensions 是可选参数,定义这个组的文件扩展名列表。例如 ['jpg', 'png', 'gif'] 表示只显示这三种扩展名的文件。
mimeTypes 是可选参数,定义这个组的 MIME 类型列表。例如 ['image/jpeg', 'image/png'] 表示只显示 JPEG 和 PNG 类型的文件。
uniformTypeIdentifiers 是可选参数,定义这个组的统一类型标识符(主要用于 iOS/macOS)。例如 ['public.image'] 表示所有图片类型。
webWildCards 是可选参数,定义这个组的网络通配符。例如 ['image/*'] 表示所有图片类型,['video/*'] 表示所有视频类型。
使用示例
dart
const XTypeGroup imageGroup = XTypeGroup(
label: '图片文件',
extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'],
mimeTypes: ['image/jpeg', 'image/png', 'image/gif'],
);
const XTypeGroup documentGroup = XTypeGroup(
label: '文档文件',
extensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'],
mimeTypes: [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
],
);
const XTypeGroup videoGroup = XTypeGroup(
label: '视频文件',
extensions: ['mp4', 'mov', 'avi', 'mkv', 'webm'],
mimeTypes: ['video/mp4', 'video/quicktime', 'video/x-msvideo'],
);
const XTypeGroup audioGroup = XTypeGroup(
label: '音频文件',
extensions: ['mp3', 'wav', 'aac', 'flac', 'ogg'],
mimeTypes: ['audio/mpeg', 'audio/wav', 'audio/aac'],
);
Future<XFile?> pickImage() async {
return await openFile(acceptedTypeGroups: [imageGroup]);
}
Future<XFile?> pickDocument() async {
return await openFile(acceptedTypeGroups: [documentGroup]);
}
Future<XFile?> pickMedia() async {
return await openFile(acceptedTypeGroups: [imageGroup, videoGroup, audioGroup]);
}
3.5 XFile 类
XFile 是跨平台文件抽象类,提供了文件的基本信息和操作方法。该类与 image_picker 库使用的 XFile 是同一个类。
常用属性
path 属性返回文件的绝对路径,类型为 String。这是访问文件的主要方式。
name 属性返回文件名(包含扩展名),类型为 String。例如 document.pdf。
mimeType 属性返回文件的 MIME 类型,类型为 String?。可能为 null,取决于平台支持。
常用方法
length() 方法返回文件大小(字节),返回 Future<int>。适用于显示文件大小或进行大小限制检查。
readAsBytes() 方法将文件内容读取为字节数组,返回 Future<Uint8List>。适用于处理二进制文件。
readAsString() 方法将文件内容读取为字符串,返回 Future<String>。适用于处理文本文件。
openRead() 方法返回文件读取流,返回 Stream<List<int>>。适用于大文件的分块读取。
lastModified() 方法返回文件最后修改时间,返回 Future<DateTime>。
使用示例
dart
Future<void> analyzeFile(XFile file) async {
final int size = await file.length();
final String fileName = file.name;
final String filePath = file.path;
final DateTime? modified = await file.lastModified();
print('文件名: $fileName');
print('文件路径: $filePath');
print('文件大小: ${(size / 1024).toStringAsFixed(2)} KB');
print('MIME类型: ${file.mimeType}');
if (modified != null) {
print('最后修改: $modified');
}
}
Future<String> readTextFile(XFile file) async {
return await file.readAsString();
}
Future<Uint8List> readBinaryFile(XFile file) async {
return await file.readAsBytes();
}
四、实战案例
4.1 完整的文件选择器组件
dart
class FilePickerWidget extends StatefulWidget {
final Function(List<XFile> files) onFilesSelected;
final List<XTypeGroup>? allowedTypes;
final bool allowMultiple;
final int maxFiles;
const FilePickerWidget({
required this.onFilesSelected,
this.allowedTypes,
this.allowMultiple = true,
this.maxFiles = 10,
Key? key,
}) : super(key: key);
@override
_FilePickerWidgetState createState() => _FilePickerWidgetState();
}
class _FilePickerWidgetState extends State<FilePickerWidget> {
List<XFile> _selectedFiles = [];
Future<void> _pickFiles() async {
if (_selectedFiles.length >= widget.maxFiles) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('最多只能选择 ${widget.maxFiles} 个文件')),
);
return;
}
List<XFile> files;
if (widget.allowMultiple) {
files = await openFiles(
acceptedTypeGroups: widget.allowedTypes ?? [],
);
} else {
final XFile? file = await openFile(
acceptedTypeGroups: widget.allowedTypes ?? [],
);
files = file != null ? [file] : [];
}
if (files.isNotEmpty) {
setState(() {
_selectedFiles.addAll(files);
if (_selectedFiles.length > widget.maxFiles) {
_selectedFiles = _selectedFiles.sublist(0, widget.maxFiles);
}
});
widget.onFilesSelected(_selectedFiles);
}
}
void _removeFile(int index) {
setState(() {
_selectedFiles.removeAt(index);
});
widget.onFilesSelected(_selectedFiles);
}
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';
}
IconData _getFileIcon(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
switch (ext) {
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'xls':
case 'xlsx':
return Icons.table_chart;
case 'ppt':
case 'pptx':
return Icons.slideshow;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return Icons.image;
case 'mp4':
case 'mov':
case 'avi':
return Icons.videocam;
case 'mp3':
case 'wav':
return Icons.audiotrack;
case 'zip':
case 'rar':
return Icons.folder_zip;
default:
return Icons.insert_drive_file;
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: _selectedFiles.length,
itemBuilder: (context, index) {
final file = _selectedFiles[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: Icon(_getFileIcon(file.name)),
title: Text(
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: FutureBuilder<int>(
future: file.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(_formatFileSize(snapshot.data!));
}
return Text('计算中...');
},
),
trailing: IconButton(
icon: Icon(Icons.close),
onPressed: () => _removeFile(index),
),
),
);
},
),
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: _pickFiles,
icon: Icon(Icons.attach_file),
label: Text('选择文件 (${_selectedFiles.length}/${widget.maxFiles})'),
),
),
],
);
}
}
4.2 文档导入功能
dart
class DocumentImporter extends StatefulWidget {
@override
_DocumentImporterState createState() => _DocumentImporterState();
}
class _DocumentImporterState extends State<DocumentImporter> {
XFile? _importedFile;
String? _fileContent;
bool _isLoading = false;
static const XTypeGroup documentTypes = XTypeGroup(
label: '文档',
extensions: ['txt', 'json', 'xml', 'csv'],
);
Future<void> _importDocument() async {
final XFile? file = await openFile(
acceptedTypeGroups: [documentTypes],
);
if (file != null) {
setState(() {
_isLoading = true;
_importedFile = file;
});
try {
final content = await file.readAsString();
setState(() {
_fileContent = content;
_isLoading = false;
});
} catch (e) {
setState(() {
_fileContent = '读取文件失败: $e';
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('文档导入'),
actions: [
IconButton(
icon: Icon(Icons.folder_open),
onPressed: _importDocument,
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _importedFile == null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.description, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('点击右上角按钮导入文档'),
],
),
)
: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'文件名: ${_importedFile!.name}',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Divider(),
SizedBox(height: 8),
Text(
'文件内容:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_fileContent ?? '',
style: TextStyle(fontFamily: 'monospace'),
),
),
],
),
),
);
}
}
4.3 目录浏览器
dart
class DirectoryBrowser extends StatefulWidget {
@override
_DirectoryBrowserState createState() => _DirectoryBrowserState();
}
class _DirectoryBrowserState extends State<DirectoryBrowser> {
String? _selectedDirectory;
Future<void> _selectDirectory() async {
final String? directory = await getDirectoryPath();
if (directory != null) {
setState(() {
_selectedDirectory = directory;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('目录浏览器')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.folder, size: 64, color: Colors.amber),
SizedBox(height: 16),
if (_selectedDirectory != null)
Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Column(
children: [
Text(
'已选择目录:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_selectedDirectory!,
textAlign: TextAlign.center,
style: TextStyle(fontFamily: 'monospace'),
),
),
],
),
)
else
Text('未选择目录'),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _selectDirectory,
icon: Icon(Icons.folder_open),
label: Text('选择目录'),
),
],
),
),
);
}
}
五、最佳实践
5.1 文件类型过滤策略
根据业务需求设置合适的文件类型过滤:
dart
const XTypeGroup images = XTypeGroup(
label: '图片',
extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
);
const XTypeGroup documents = XTypeGroup(
label: '文档',
extensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'],
);
const XTypeGroup allSupported = XTypeGroup(
label: '所有支持的文件',
extensions: [
'jpg', 'jpeg', 'png', 'gif', 'webp',
'pdf', 'doc', 'docx', 'xls', 'xlsx',
],
);
Future<XFile?> pickImage() => openFile(acceptedTypeGroups: [images]);
Future<XFile?> pickDocument() => openFile(acceptedTypeGroups: [documents]);
Future<XFile?> pickAny() => openFile(acceptedTypeGroups: [allSupported]);
5.2 文件大小检查
在选择文件后检查文件大小:
dart
Future<bool> checkFileSize(XFile file, int maxSizeMB) async {
final int size = await file.length();
final int maxSizeBytes = maxSizeMB * 1024 * 1024;
if (size > maxSizeBytes) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('文件大小不能超过 ${maxSizeMB}MB')),
);
return false;
}
return true;
}
Future<void> _pickAndCheckFile() async {
final XFile? file = await openFile();
if (file != null) {
if (await checkFileSize(file, 10)) {
setState(() {
_selectedFile = file;
});
}
}
}
5.3 错误处理
对所有文件操作进行错误处理:
dart
Future<void> _pickFileWithErrorHandling() async {
try {
final XFile? file = await openFile();
if (file != null) {
setState(() {
_selectedFile = file;
});
}
} on PlatformException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('选择文件失败: ${e.message}')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('发生未知错误: $e')),
);
}
}
六、常见问题
Q1:如何获取文件的扩展名?
使用 Dart 的 path 包:
dart
import 'package:path/path.dart' as path;
String getExtension(XFile file) {
return path.extension(file.path).toLowerCase();
}
String getExtensionWithoutDot(XFile file) {
return path.extension(file.path).replaceAll('.', '').toLowerCase();
}
Q2:如何判断文件类型?
通过扩展名或 MIME 类型判断:
dart
bool isImage(XFile file) {
final ext = path.extension(file.path).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext);
}
bool isDocument(XFile file) {
final ext = path.extension(file.path).toLowerCase();
return ['.pdf', '.doc', '.docx', '.xls', '.xlsx'].contains(ext);
}
bool isVideo(XFile file) {
final ext = path.extension(file.path).toLowerCase();
return ['.mp4', '.mov', '.avi', '.mkv'].contains(ext);
}
Q3:如何上传文件到服务器?
使用 http 包上传:
dart
Future<void> uploadFile(XFile file) async {
final uri = Uri.parse('https://your-server.com/upload');
final request = http.MultipartRequest('POST', uri);
final stream = http.ByteStream(file.openRead());
final length = await file.length();
final multipartFile = http.MultipartFile(
'file',
stream,
length,
filename: file.name,
);
request.files.add(multipartFile);
final response = await request.send();
if (response.statusCode == 200) {
print('上传成功');
}
}
Q4:OpenHarmony 平台支持保存文件吗?
目前 OpenHarmony 平台的 file_selector 不支持 getSaveLocation 方法(选择保存位置)。如果需要保存文件,可以使用 path_provider 库获取应用目录,然后直接写入文件。
七、总结
file_selector 库为 Flutter for OpenHarmony 开发提供了完整的文件选择功能。通过统一的 API 接口,开发者可以轻松实现单文件选择、多文件选择、目录选择等功能。该库支持丰富的文件类型过滤选项,能够满足各种业务场景的需求。在鸿蒙平台上,file_selector 已经完成了完整的适配,开发者可以放心使用。
八、完整代码示例

以下是一个完整的可运行示例,展示了 file_selector 库的所有核心功能:
pubspec.yaml
yaml
name: file_selector_demo
description: Flutter for OpenHarmony file_selector 演示项目
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
file_selector:
git:
url: https://gitcode.com/openharmony-tpc/flutter_packages.git
path: packages/file_selector/file_selector
flutter:
uses-material-design: true
main.dart
dart
import 'package:flutter/material.dart';
import 'package:file_selector/file_selector.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'File Selector Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('File Selector 文件选择演示'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
children: [
_buildMenuItem(
context,
'单文件选择',
'选择单个文件并查看详情',
Icons.insert_drive_file,
() => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SingleFileDemo()),
),
),
_buildMenuItem(
context,
'多文件选择',
'一次选择多个文件',
Icons.folder_open,
() => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MultiFileDemo()),
),
),
_buildMenuItem(
context,
'文件类型过滤',
'按文件类型筛选选择',
Icons.filter_list,
() => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FileTypeFilterDemo()),
),
),
_buildMenuItem(
context,
'目录选择',
'选择目录路径',
Icons.folder,
() => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DirectorySelectorDemo()),
),
),
_buildMenuItem(
context,
'文档导入',
'导入并读取文本文件',
Icons.description,
() => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DocumentImportDemo()),
),
),
],
),
);
}
Widget _buildMenuItem(
BuildContext context,
String title,
String subtitle,
IconData icon,
VoidCallback onTap,
) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Icon(icon, size: 32, color: Theme.of(context).primaryColor),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(subtitle),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: onTap,
),
);
}
}
class SingleFileDemo extends StatefulWidget {
const SingleFileDemo({super.key});
@override
State<SingleFileDemo> createState() => _SingleFileDemoState();
}
class _SingleFileDemoState extends State<SingleFileDemo> {
XFile? _selectedFile;
Future<void> _pickFile() async {
final XFile? file = await openFile();
if (file != null) {
setState(() => _selectedFile = file);
}
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
IconData _getFileIcon(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
switch (ext) {
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'xls':
case 'xlsx':
return Icons.table_chart;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return Icons.image;
case 'mp4':
case 'mov':
return Icons.videocam;
case 'mp3':
case 'wav':
return Icons.audiotrack;
default:
return Icons.insert_drive_file;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('单文件选择'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: _selectedFile == null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.insert_drive_file, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text('未选择文件'),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _pickFile,
icon: const Icon(Icons.folder_open),
label: const Text('选择文件'),
),
],
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(
_getFileIcon(_selectedFile!.name),
size: 64,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 16),
Text(
_selectedFile!.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
FutureBuilder<int>(
future: _selectedFile!.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
'大小: ${_formatFileSize(snapshot.data!)}',
style: TextStyle(color: Colors.grey[600]),
);
}
return const Text('计算中...');
},
),
const SizedBox(height: 8),
Text(
'路径: ${_selectedFile!.path}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _pickFile,
icon: const Icon(Icons.folder_open),
label: const Text('重新选择'),
),
],
),
),
),
);
}
}
class MultiFileDemo extends StatefulWidget {
const MultiFileDemo({super.key});
@override
State<MultiFileDemo> createState() => _MultiFileDemoState();
}
class _MultiFileDemoState extends State<MultiFileDemo> {
List<XFile> _selectedFiles = [];
Future<void> _pickFiles() async {
final List<XFile> files = await openFiles();
if (files.isNotEmpty) {
setState(() => _selectedFiles = files);
}
}
IconData _getFileIcon(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
switch (ext) {
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'jpg':
case 'jpeg':
case 'png':
return Icons.image;
default:
return Icons.insert_drive_file;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('多文件选择'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column(
children: [
Expanded(
child: _selectedFiles.isEmpty
? const Center(child: Text('未选择文件'))
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _selectedFiles.length,
itemBuilder: (context, index) {
final file = _selectedFiles[index];
return Card(
child: ListTile(
leading: Icon(_getFileIcon(file.name)),
title: Text(file.name),
subtitle: Text(
file.path,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: _pickFiles,
icon: const Icon(Icons.folder_open),
label: Text('选择文件 (${_selectedFiles.length})'),
),
),
],
),
);
}
}
class FileTypeFilterDemo extends StatefulWidget {
const FileTypeFilterDemo({super.key});
@override
State<FileTypeFilterDemo> createState() => _FileTypeFilterDemoState();
}
class _FileTypeFilterDemoState extends State<FileTypeFilterDemo> {
XFile? _selectedFile;
String _selectedType = '图片';
static const Map<String, XTypeGroup> _typeGroups = {
'图片': XTypeGroup(
label: '图片',
extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
),
'文档': XTypeGroup(
label: '文档',
extensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'],
),
'视频': XTypeGroup(
label: '视频',
extensions: ['mp4', 'mov', 'avi', 'mkv'],
),
'音频': XTypeGroup(
label: '音频',
extensions: ['mp3', 'wav', 'aac', 'flac'],
),
};
Future<void> _pickFile() async {
final typeGroup = _typeGroups[_selectedType];
final XFile? file = await openFile(
acceptedTypeGroups: [typeGroup!],
);
if (file != null) {
setState(() => _selectedFile = file);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('文件类型过滤'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
DropdownButtonFormField<String>(
value: _selectedType,
decoration: const InputDecoration(
labelText: '文件类型',
border: OutlineInputBorder(),
),
items: _typeGroups.keys.map((type) {
return DropdownMenuItem(value: type, child: Text(type));
}).toList(),
onChanged: (value) {
setState(() {
_selectedType = value!;
_selectedFile = null;
});
},
),
const SizedBox(height: 24),
if (_selectedFile != null)
Card(
child: ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(_selectedFile!.name),
subtitle: Text(_selectedFile!.path),
),
)
else
const Text('未选择文件'),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _pickFile,
icon: const Icon(Icons.folder_open),
label: Text('选择 $_selectedType'),
),
],
),
),
);
}
}
class DirectorySelectorDemo extends StatefulWidget {
const DirectorySelectorDemo({super.key});
@override
State<DirectorySelectorDemo> createState() => _DirectorySelectorDemoState();
}
class _DirectorySelectorDemoState extends State<DirectorySelectorDemo> {
String? _selectedDirectory;
Future<void> _selectDirectory() async {
final String? directory = await getDirectoryPath();
if (directory != null) {
setState(() => _selectedDirectory = directory);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('目录选择'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.folder, size: 64, color: Colors.amber),
const SizedBox(height: 16),
if (_selectedDirectory != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
children: [
const Text(
'已选择目录:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_selectedDirectory!,
textAlign: TextAlign.center,
style: const TextStyle(fontFamily: 'monospace'),
),
),
],
),
)
else
const Text('未选择目录'),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _selectDirectory,
icon: const Icon(Icons.folder_open),
label: const Text('选择目录'),
),
],
),
),
);
}
}
class DocumentImportDemo extends StatefulWidget {
const DocumentImportDemo({super.key});
@override
State<DocumentImportDemo> createState() => _DocumentImportDemoState();
}
class _DocumentImportDemoState extends State<DocumentImportDemo> {
XFile? _importedFile;
String? _fileContent;
bool _isLoading = false;
static const XTypeGroup documentTypes = XTypeGroup(
label: '文档',
extensions: ['txt', 'json', 'xml', 'csv', 'md'],
);
Future<void> _importDocument() async {
final XFile? file = await openFile(acceptedTypeGroups: [documentTypes]);
if (file != null) {
setState(() {
_isLoading = true;
_importedFile = file;
});
try {
final content = await file.readAsString();
setState(() {
_fileContent = content;
_isLoading = false;
});
} catch (e) {
setState(() {
_fileContent = '读取文件失败: $e';
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('文档导入'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.folder_open),
onPressed: _importDocument,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _importedFile == null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.description, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
const Text('点击右上角按钮导入文档'),
],
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'文件名: ${_importedFile!.name}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Divider(),
const SizedBox(height: 8),
const Text(
'文件内容:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_fileContent ?? '',
style: const TextStyle(fontFamily: 'monospace'),
),
),
],
),
),
);
}
}
