Flutter_OpenHarmony_三方库_file_selector文件选择适配详解

欢迎加入开源鸿蒙跨平台社区: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 平台特性

  1. 权限处理

    • file_selector 使用系统原生的文件选择器(PhotoViewPicker、DocumentViewPicker)
    • 选择器会自动处理文件访问权限,无需额外申请媒体权限
    • 仅在需要访问网络资源时才需要配置网络权限
  2. 文件路径处理

    • OpenHarmony 平台返回的文件路径为沙箱路径
    • 路径仅在应用生命周期内有效,不应跨会话保存
    • 如需持久化存储,应使用 saveTo 方法复制文件到持久化目录
  3. 文件类型过滤

    • OpenHarmony 平台支持 extensions 和 mimeTypes 过滤
    • uniformTypeIdentifiers 会被映射到系统对应的类型标识符
    • 空列表表示不限制文件类型
  4. 目录选择

    • OpenHarmony 平台支持目录选择功能
    • 返回的目录路径可用于后续文件操作
    • 不支持多目录选择(getDirectoryPaths 可能返回空或单元素列表)

5.2 与 Android/iOS 的差异

差异点 Android/iOS OpenHarmony
权限申请 运行时动态申请 声明式权限,选择器自动处理
文件路径 持久化路径 沙箱临时路径
保存位置选择 部分平台支持 不支持
文件类型过滤 完整支持 支持,但映射方式不同
初始目录 支持 支持,但可能被系统忽略
自定义按钮文本 支持 支持

5.3 注意事项

  1. 不要保存文件路径

    • XFile 的路径仅在应用会话内有效
    • 跨会话使用应复制文件到持久化目录
  2. 错误处理

    • 所有方法都可能抛出异常
    • 建议使用 try-catch 包裹调用代码
  3. 文件类型组配置

    • 至少提供一种过滤方式(extensions 或 mimeTypes)
    • 多个类型组会在选择器中显示为可切换的过滤选项
  4. 大文件处理

    • 使用 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 平台的适配已经非常成熟。通过本文的介绍,你应该已经掌握了:

  1. file_selector 的核心 API 和使用方法
  2. XTypeGroup 文件类型过滤的配置方式
  3. 完整的文件选择应用实现
  4. OpenHarmony 平台的权限配置和适配要点
  5. 常见问题和解决方案

在实际开发中,建议根据具体需求合理配置文件类型过滤,并注意文件路径的临时性特点。对于需要持久化存储的场景,务必使用 saveTo 方法将文件复制到应用的持久化目录中。

💡 提示: 更多 OpenHarmony 适配的 Flutter 三方库信息,请访问 开源鸿蒙跨平台开发者社区 获取最新资源和技术支持。

相关推荐
陆业聪2 小时前
跨端框架横评 2026:Flutter、React Native、KMP、Kuikly、小程序,谁是你下一个项目的正确答案?
flutter·大前端·跨端开发
2601_949593652 小时前
Flutter_OpenHarmony_三方库_url_launcher链接跳转适配详解
flutter
天渺工作室3 小时前
Flutter 版的 NVM——FVM 使用指南
flutter·dart
Lanren的编程日记13 小时前
Flutter鸿蒙应用开发:生物识别(指纹/面容)功能集成实战
flutter·华为·harmonyos
Lanren的编程日记17 小时前
Flutter鸿蒙应用开发:基础UI组件库设计与实现实战
flutter·ui·harmonyos
西西学代码17 小时前
Flutter---波形动画
flutter
于慨20 小时前
flutter基础组件用法
开发语言·javascript·flutter
恋猫de小郭1 天前
Android CLI ,谷歌为 Android 开发者专研的 AI Agent,提速三倍
android·前端·flutter
火柴就是我1 天前
flutter pushAndRemoveUntil 的一次小疑惑
flutter