Flutter for OpenHarmony 文件管理器应用开发实战

作者:maaath

社区引导信息

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net


前言

随着 Flutter for OpenHarmony 生态的持续发展,越来越多的 Flutter 应用成功适配到鸿蒙设备上。本文将通过一个完整的文件管理器应用实例,带领读者深入了解如何使用 Flutter 开发适用于 OpenHarmony 设备的跨平台应用,从项目架构设计到核心功能实现,全方位展示 Flutter 跨平台开发的魅力。

一、项目概述

文件管理器是移动设备中最常用的系统应用之一,也是展示跨平台开发能力的经典案例。本文将基于 Flutter 框架,开发一个功能完备的文件管理器应用,涵盖文件浏览、搜索、复制移动删除、文件夹管理、排序切换、详情查看、存储统计和文件分类展示等核心功能。

1.1 技术栈

  • 框架:Flutter 3.x for OpenHarmony
  • 语言:Dart
  • 状态管理: StatefulWidget(适合本应用复杂度)
  • 架构模式:三层架构(UI层 / 业务层 / 数据层)

1.2 功能清单

功能模块 描述
文件浏览与导航 支持前进、后退、跳转父目录、面包屑导航
文件搜索与筛选 实时搜索文件、隐藏文件显示切换
文件复制/移动/删除 完整的文件操作支持
文件夹创建与管理 新建文件夹、重命名
文件排序 按名称、日期、大小、类型排序
文件详情查看 侧边栏显示文件属性
存储空间统计 存储概览页面
文件类型分类 图片、视频、文档等分类展示

二、项目结构设计

良好的项目结构是应用可维护性的基础。本文采用清晰的分层架构:

lib/
├── main.dart                      // 应用入口
├── models/                        // 数据模型层
│   ├── file_item.dart            // 文件/文件夹模型
│   └── storage_info.dart         // 存储信息模型
├── services/                      // 业务服务层
│   └── file_service.dart         // 文件系统服务
├── screens/                       // 页面层
│   └── file_manager_screen.dart  // 主页面
├── widgets/                       // 组件层
│   ├── file_list_item.dart      // 文件列表项
│   ├── storage_card.dart         // 存储卡片
│   ├── category_card.dart        // 分类卡片
│   ├── file_detail_panel.dart    // 详情面板
│   ├── breadcrumb_navigation.dart// 面包屑导航
│   ├── search_bar_widget.dart   // 搜索组件
│   └── sort_options_sheet.dart   // 排序选项
└── utils/                         // 工具层
    └── format_utils.dart         // 格式化工具

这种分层结构的优势在于:

  • 模型层:定义数据结构,与 UI 完全解耦
  • 服务层:封装文件系统操作,便于测试和复用
  • 组件层:原子化组件,提高复用性
  • 页面层:组合组件,处理业务逻辑

三、核心模型设计

3.1 文件模型

文件模型是整个应用的基础数据结构,需要包含文件的所有关键属性:

enum FileItemType {
  file,
  directory,
}

enum SortType {
  name,
  date,
  size,
  type,
}

enum SortOrder {
  ascending,
  descending,
}

class FileItem {
  final String name;
  final String path;
  final FileItemType type;
  final int size;
  final DateTime modifiedTime;
  final DateTime createdTime;
  final bool isHidden;

  FileItem({
    required this.name,
    required this.path,
    required this.type,
    required this.size,
    required this.modifiedTime,
    required this.createdTime,
    this.isHidden = false,
  });
}

3.2 存储信息模型

用于展示设备存储状态:

class StorageInfo {
  final int totalSpace;
  final int usedSpace;
  final int freeSpace;

  double get usedPercentage {
    if (totalSpace == 0) return 0;
    return usedSpace / totalSpace;
  }
}

四、文件系统服务实现

服务层是连接应用逻辑与系统底层的桥梁。以下是文件服务的核心实现:

class FileService {
  static final FileService _instance = FileService._internal();
  factory FileService() => _instance;
  FileService._internal();

  String _currentPath = '';

  Future<List<FileItem>> listDirectory(String path) async {
    final directory = Directory(path);
    if (!await directory.exists()) {
      throw Exception('Directory does not exist: $path');
    }

    _currentPath = path;
    final items = <FileItem>[];

    await for (final entity in directory.list(followLinks: false)) {
      try {
        final stat = await entity.stat();
        final name = entity.path.split(Platform.pathSeparator).last;

        items.add(FileItem(
          name: name,
          path: entity.path,
          type: entity is Directory ? FileItemType.directory : FileItemType.file,
          size: stat.size,
          modifiedTime: stat.modified,
          createdTime: stat.changed,
          isHidden: name.startsWith('.'),
        ));
      } catch (e) {
        continue;
      }
    }
    return items;
  }
}

4.1 文件操作方法

服务层封装了常用的文件操作:

// 复制文件
Future<void> copyFile(String sourcePath, String destinationPath) async {
  final sourceFile = File(sourcePath);
  await sourceFile.copy(destinationPath);
}

// 移动文件
Future<void> moveFile(String sourcePath, String destinationPath) async {
  final sourceFile = File(sourcePath);
  await sourceFile.rename(destinationPath);
}

// 删除文件
Future<void> deleteFile(String path) async {
  final file = File(path);
  await file.delete();
}

// 创建目录
Future<void> createDirectory(String path) async {
  final directory = Directory(path);
  await directory.create(recursive: true);
}

4.2 文件搜索实现

搜索功能支持递归遍历目录:

Future<List<FileItem>> searchFiles(String query, {String? path}) async {
  final searchPath = path ?? _currentPath;
  final directory = Directory(searchPath);
  final results = <FileItem>[];

  await for (final entity in directory.list(recursive: true, followLinks: false)) {
    final name = entity.path.split(Platform.pathSeparator).last;
    if (name.toLowerCase().contains(query.toLowerCase())) {
      final stat = await entity.stat();
      results.add(FileItem(
        name: name,
        path: entity.path,
        type: entity is Directory ? FileItemType.directory : FileItemType.file,
        size: stat.size,
        modifiedTime: stat.modified,
        createdTime: stat.changed,
      ));
    }
  }
  return results;
}

4.3 文件类型分类

根据扩展名将文件分类统计:

Future<List<FileTypeCategory>> getFileTypeCategories({String? path}) async {
  final searchPath = path ?? _currentPath;
  final directory = Directory(searchPath);
  final categories = <String, List<FileItem>>{};

  final categoryMapping = {
    'Images': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'],
    'Videos': ['mp4', 'avi', 'mkv', 'mov', 'wmv'],
    'Audio': ['mp3', 'wav', 'flac', 'aac', 'ogg'],
    'Documents': ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'md'],
    'Archives': ['zip', 'rar', '7z', 'tar', 'gz'],
    'Code': ['dart', 'java', 'py', 'js', 'ts', 'json', 'xml'],
  };

  await for (final entity in directory.list(recursive: true, followLinks: false)) {
    if (entity is File) {
      final name = entity.path.split(Platform.pathSeparator).last;
      final ext = _getExtension(name);
      final category = _getCategoryForExt(ext, categoryMapping);

      categories.putIfAbsent(category, () => []).add(FileItem(...));
    }
  }

  return categories.entries.map((entry) {
    return FileTypeCategory(
      name: entry.key,
      count: entry.value.length,
      totalSize: entry.value.fold(0, (sum, item) => sum + item.size),
    );
  }).toList();
}

五、UI 组件开发

5.1 文件列表项组件

采用简洁的列表设计,带有选中态反馈:

class FileListItem extends StatelessWidget {
  final FileItem item;
  final bool isSelected;
  final VoidCallback onTap;
  final VoidCallback onLongPress;

  const FileListItem({
    super.key,
    required this.item,
    required this.isSelected,
    required this.onTap,
    required this.onLongPress,
  });

  
  Widget build(BuildContext context) {
    return Material(
      color: isSelected
          ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
          : Colors.transparent,
      child: InkWell(
        onTap: onTap,
        onLongPress: onLongPress,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          child: Row(
            children: [
              Container(
                width: 48,
                height: 48,
                decoration: BoxDecoration(
                  color: item.iconColor.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(item.icon, color: item.iconColor, size: 24),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(item.name, style: const TextStyle(fontSize: 15)),
                    Text(
                      item.type == FileItemType.directory ? 'Folder' : item.displaySize,
                      style: TextStyle(fontSize: 13, color: Colors.grey[600]),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5.2 存储卡片组件

可视化展示存储空间使用情况:

class StorageCard extends StatelessWidget {
  final StorageInfo storageInfo;

  const StorageCard({super.key, required this.storageInfo});

  
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Row(
              children: [
                const Icon(Icons.storage, size: 32),
                const SizedBox(width: 16),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('Storage', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                    Text('${storageInfo.usedFormatted} / ${storageInfo.totalFormatted}'),
                  ],
                ),
              ],
            ),
            const SizedBox(height: 20),
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: LinearProgressIndicator(
                value: storageInfo.usedPercentage,
                minHeight: 8,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

5.3 文件详情面板

侧边栏展示选中文件的完整信息:

class FileDetailPanel extends StatelessWidget {
  final FileItem? selectedFile;

  const FileDetailPanel({super.key, this.selectedFile});

  
  Widget build(BuildContext context) {
    if (selectedFile == null) {
      return const Center(child: Text('Select a file to view details'));
    }

    return Container(
      width: 300,
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Container(
            width: 100,
            height: 100,
            decoration: BoxDecoration(
              color: selectedFile!.iconColor.withOpacity(0.1),
              borderRadius: BorderRadius.circular(20),
            ),
            child: Icon(selectedFile!.icon, size: 50, color: selectedFile!.iconColor),
          ),
          const SizedBox(height: 20),
          Text(selectedFile!.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
          const SizedBox(height: 24),
          _buildInfoRow('Location', selectedFile!.path),
          _buildInfoRow('Modified', FormatUtils.formatDate(selectedFile!.modifiedTime)),
          _buildInfoRow('Created', FormatUtils.formatDate(selectedFile!.createdTime)),
        ],
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: Row(
        children: [
          Text('$label: ', style: const TextStyle(color: Colors.grey)),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }
}

5.4 面包屑导航组件

支持快速跳转到任意父目录:

class BreadcrumbNavigation extends StatelessWidget {
  final String currentPath;
  final Function(String) onNavigate;

  const BreadcrumbNavigation({
    super.key,
    required this.currentPath,
    required this.onNavigate,
  });

  
  Widget build(BuildContext context) {
    final parts = currentPath.split('/').where((p) => p.isNotEmpty).toList();

    return ListView.separated(
      scrollDirection: Axis.horizontal,
      itemCount: parts.length,
      separatorBuilder: (_, __) => const Icon(Icons.chevron_right),
      itemBuilder: (context, index) {
        final path = '/${parts.sublist(0, index + 1).join('/')}';
        final isLast = index == parts.length - 1;

        return Center(
          child: InkWell(
            onTap: isLast ? null : () => onNavigate(path),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              child: Text(
                index == 0 ? 'Home' : parts[index],
                style: TextStyle(
                  fontWeight: isLast ? FontWeight.bold : FontWeight.normal,
                  color: isLast ? Theme.of(context).colorScheme.primary : null,
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

六、主页面业务逻辑

主页面整合所有组件,处理核心业务逻辑:

class FileManagerScreen extends StatefulWidget {
  const FileManagerScreen({super.key});

  
  State<FileManagerScreen> createState() => _FileManagerScreenState();
}

class _FileManagerScreenState extends State<FileManagerScreen> {
  final FileService _fileService = FileService();

  String _currentPath = '/';
  List<FileItem> _files = [];
  List<FileItem> _filteredFiles = [];
  bool _isLoading = true;

  // 排序状态
  SortType _sortType = SortType.name;
  SortOrder _sortOrder = SortOrder.ascending;

  // 搜索状态
  String _searchQuery = '';

  // 选中状态
  Set<String> _selectedFiles = {};
  FileItem? _selectedFile;

  
  void initState() {
    super.initState();
    _loadDirectory(_currentPath);
  }

  void _applyFiltersAndSort() {
    List<FileItem> result = List.from(_files);

    // 搜索过滤
    if (_searchQuery.isNotEmpty) {
      result = result.where((f) =>
        f.name.toLowerCase().contains(_searchQuery.toLowerCase())
      ).toList();
    }

    // 排序
    result.sort((a, b) {
      // 文件夹优先
      if (a.type != b.type) {
        return a.type == FileItemType.directory ? -1 : 1;
      }

      int comparison;
      switch (_sortType) {
        case SortType.name:
          comparison = a.name.compareTo(b.name);
          break;
        case SortType.date:
          comparison = a.modifiedTime.compareTo(b.modifiedTime);
          break;
        case SortType.size:
          comparison = a.size.compareTo(b.size);
          break;
        case SortType.type:
          comparison = a.extension.compareTo(b.extension);
          break;
      }

      return _sortOrder == SortOrder.ascending ? comparison : -comparison;
    });

    _filteredFiles = result;
  }

  Future<void> _loadDirectory(String path) async {
    setState(() => _isLoading = true);

    try {
      final files = await _fileService.listDirectory(path);
      setState(() {
        _currentPath = path;
        _files = files;
        _applyFiltersAndSort();
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
    }
  }

  void _onFileTap(FileItem item) {
    if (_selectedFiles.isNotEmpty) {
      _toggleSelection(item);
      return;
    }

    if (item.type == FileItemType.directory) {
      _loadDirectory(item.path);
    } else {
      setState(() => _selectedFile = item);
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('File Manager')),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _filteredFiles.length,
              itemBuilder: (context, index) {
                final item = _filteredFiles[index];
                return FileListItem(
                  item: item,
                  isSelected: _selectedFiles.contains(item.path),
                  onTap: () => _onFileTap(item),
                  onLongPress: () => _toggleSelection(item),
                );
              },
            ),
    );
  }
}

七、关键功能实现

7.1 批量选择与操作

长按进入多选模式,支持批量复制、移动、删除:

void _toggleSelection(FileItem item) {
  setState(() {
    if (_selectedFiles.contains(item.path)) {
      _selectedFiles.remove(item.path);
    } else {
      _selectedFiles.add(item.path);
    }
  });
}

void _deleteSelected() async {
  final confirmed = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Delete'),
      content: Text('Delete ${_selectedFiles.length} items?'),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
        FilledButton(
          onPressed: () => Navigator.pop(context, true),
          style: FilledButton.styleFrom(backgroundColor: Colors.red),
          child: const Text('Delete'),
        ),
      ],
    ),
  );

  if (confirmed == true) {
    for (final path in _selectedFiles) {
      await _fileService.deleteFile(path);
    }
    setState(() => _selectedFiles.clear());
    _loadDirectory(_currentPath);
  }
}

7.2 排序功能

支持多种排序方式和升降序切换:

void _showSortOptions() {
  showModalBottomSheet(
    context: context,
    builder: (context) => SortOptionsSheet(
      currentSortType: _sortType,
      currentSortOrder: _sortOrder,
      onSortChanged: (type, order) {
        setState(() {
          _sortType = type;
          _sortOrder = order;
          _applyFiltersAndSort();
        });
      },
    ),
  );
}

7.3 新建文件夹

Future<void> _createFolder() async {
  final nameController = TextEditingController();

  final result = await showDialog<String>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Create New Folder'),
      content: TextField(
        controller: nameController,
        decoration: const InputDecoration(labelText: 'Folder Name'),
        autofocus: true,
      ),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
        FilledButton(
          onPressed: () => Navigator.pop(context, nameController.text),
          child: const Text('Create'),
        ),
      ],
    ),
  );

  if (result != null && result.isNotEmpty) {
    final newPath = '$_currentPath/${result}';
    await _fileService.createDirectory(newPath);
    _loadDirectory(_currentPath);
  }
}

八、应用主题配置

采用 Material Design 3 设计语言,支持浅色/深色主题自动切换:

MaterialApp(
  title: 'File Manager',
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF6750A4),
      brightness: Brightness.light,
    ),
    useMaterial3: true,
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF6750A4),
      brightness: Brightness.dark,
    ),
    useMaterial3: true,
  ),
  themeMode: ThemeMode.system,
  home: const FileManagerScreen(),
)

九、截图运行

以下为应用在鸿蒙设备上的运行截图:

9.1 主界面

在这里插入图片描述

9.3 文件创建页面

在这里插入图片描述

十、总结与展望

本文通过一个完整的文件管理器应用,详细介绍了 Flutter for OpenHarmony 开发的核心技术要点:

  1. 分层架构设计:清晰的模型层、服务层、组件层、页面层划分,提高了代码的可维护性和可测试性

  2. 文件系统操作:封装了文件浏览、搜索、复制、移动、删除等核心操作

  3. 丰富的 UI 组件:文件列表、存储卡片、分类卡片、详情面板等组件,支持灵活组合

  4. 完善的交互体验:多选模式、排序切换、面包屑导航等特性,提升了用户操作效率

10.1 可扩展方向

  • 添加文件预览功能(图片缩略图、文本预览)
  • 支持文件压缩与解压
  • 实现云存储同步
  • 增加文件加密功能
  • 支持外接存储设备(USB、SD卡)

10.2 开源仓库

本文完整代码已开源至 AtomGit 平台:

仓库地址:https://atomgit.com/maaath/flutter_file_manager


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐