欢迎加入开源鸿蒙跨平台社区: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 访问网络资源

⚠️ 注意

  1. 文件选择器使用的是系统原生文件选择对话框,会自动处理文件访问权限
  2. 权限配置必须包含 reasonusedScene 字段,否则会导致应用安装失败

三、项目配置与安装

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 平台上无法使用文件选择器。

解决方案

  1. 设置平台实例:确保在 main.dart 中设置了平台实例:
import 'package:file_selector_ohos/file_selector_ohos.dart';

void main() {
  FileSelectorPlatform.instance = FileSelectorOhos();
  runApp(const MyApp());
}
  1. 配置网络权限:在 ohos/entry/src/main/module.json5 中添加完整的权限配置:
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET",
    "reason": "$string:network_reason",
    "usedScene": {
      "abilities": [
        "EntryAbility"
      ],
      "when": "inuse"
    }
  }
]
  1. 添加权限说明:在 ohos/entry/src/main/resources/base/element/string.json 中添加:
{
  "name": "network_reason",
  "value": "使用网络访问文件资源"
}

⚠️ 注意:权限配置必须包含 reasonusedScene 字段,否则会导致应用安装失败(错误代码 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 文件选择插件的使用方法,包括:

  1. 基础概念:file_selector 的特点、功能对比、文件类型过滤
  2. 平台适配:兼容性信息、权限配置
  3. 项目配置:依赖添加、平台实例设置、权限配置
  4. 核心 API:文件类型组、单文件选择、多文件选择、目录选择
  5. 实际应用:完整的文件选择应用示例
  6. 最佳实践:用户取消处理、文件类型过滤、文件信息显示、错误处理

file_selector 是一个功能完整的文件选择插件,适合各种需要文件选择功能的应用。

OpenHarmony 平台注意事项

  • main.dart 中设置平台实例(FileSelectorPlatform.instance = FileSelectorOhos()
  • 必须配置完整的网络权限(ohos.permission.INTERNET),包含 reasonusedScene 字段
  • **目录选择功能(getDirectoryPath)在原始版本中未实现
  • 不支持选择保存位置(getSaveLocation

十一、参考资料

📌 提示:本文基于 Flutter 3.27.5 和 file_selector@1.0.3 编写,不同版本可能略有差异。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐