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

在这里插入图片描述

1. 简介

flutter_filereader 是一个功能强大的本地文件查看器库,能够在 Flutter 应用中直接预览多种格式的本地文件。该库通过平台原生能力实现文件渲染,Android 端基于腾讯 X5 内核,iOS 端基于 WKWebView,OpenHarmony 端则通过原生 WebView 实现文件查看功能。

在 OpenHarmony 环境下,flutter_filereader 可以帮助开发者实现:

  • 查看本地文档文件(TXT、PDF、Word、Excel、PPT 等)
  • 内置加载状态管理
  • 自定义加载中和不支持文件的提示界面
  • 支持文件打开状态回调
  • 跨平台兼容(Android、iOS、OpenHarmony)

核心特性

  • 多格式支持:支持 TXT、PDF、Word、Excel、PPT 等常见办公文档格式
  • 原生渲染:基于平台原生能力,渲染效果好
  • 状态管理:内置引擎加载状态、文件状态管理
  • 自定义 UI:支持自定义加载中和不支持文件的提示界面
  • 异步加载:引擎异步加载,不阻塞主线程
  • 跨平台:一套代码适配 Android、iOS、OpenHarmony

适用场景

  • 办公文档查看应用
  • 文件管理器内置预览
  • 在线教育平台课件查看
  • 企业 OA 系统附件查看
  • 电子书阅读器

支持的文件类型

平台 支持格式
OpenHarmony TXT
Android DOCX、DOC、XLSX、XLS、PPTX、PPT、PDF、TXT
iOS DOCX、DOC、XLSX、XLS、PPTX、PPT、PDF、TXT、JPG、JPEG、PNG

源码仓库

  • AtomGit: https://atomgit.com/openharmony-sig/fluttertpc_flutter_filereader.git

2. 核心 API 讲解

2.1 FileReaderView 组件

2.1.1 构造函数
FileReaderView({
  Key? key,
  required this.filePath,
  this.openSuccess,
  this.loadingWidget,
  this.unSupportFileWidget,
})

参数说明:

参数名 类型 必填 默认值 说明
filePath String - 本地文件路径,必须是绝对路径
openSuccess Function(bool)? null 文件打开成功回调,参数为是否成功
loadingWidget Widget? null 自定义加载中的提示组件
unSupportFileWidget Widget? null 自定义不支持文件的提示组件

功能说明:

FileReaderView 是核心组件,用于显示本地文件内容。组件初始化时会检查文件是否存在,然后加载原生引擎并打开文件。

内部状态流转:

LOADING_ENGINE → ENGINE_LOAD_SUCCESS → 显示文件内容
                      ↓
                 UNSUPPORT_FILE (不支持的文件类型)
                      ↓
                 FILE_NOT_FOUND (文件不存在)
                      ↓
                 ENGINE_LOAD_FAIL (引擎加载失败)

使用场景:

  • 查看本地文档文件
  • 文件管理器中的文件预览
  • 下载完成后的文件查看

使用示例:

// 基础用法
FileReaderView(
  filePath: '/path/to/file.txt',
)

// 自定义加载中提示
FileReaderView(
  filePath: '/path/to/file.txt',
  loadingWidget: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CircularProgressIndicator(),
        SizedBox(height: 16),
        Text('正在加载文件...'),
      ],
    ),
  ),
)

// 自定义不支持文件提示
FileReaderView(
  filePath: '/path/to/file.unknown',
  unSupportFileWidget: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.error_outline, size: 64, color: Colors.orange),
        SizedBox(height: 16),
        Text('暂不支持此文件格式'),
      ],
    ),
  ),
)

// 监听打开状态
FileReaderView(
  filePath: '/path/to/file.txt',
  openSuccess: (success) {
    if (success) {
      print('文件打开成功');
    } else {
      print('文件打开失败');
    }
  },
)

2.2 FileReader 单例类

2.2.1 engineLoadStatus 方法
void engineLoadStatus(Function(bool)? loadCallback)

参数说明:

参数名 类型 必填 说明
loadCallback Function(bool)? 引擎加载状态回调,true 表示成功,false 表示失败

功能说明:

检查原生引擎的加载状态。引擎加载有三种状态:

  • -1:正在加载中,会设置方法调用处理器监听加载完成事件
  • 5:加载成功,直接回调成功
  • 10:加载失败,直接回调失败

使用场景:

  • 在打开文件前检查引擎是否就绪
  • 显示引擎加载进度

使用示例:

FileReader.instance.engineLoadStatus((success) {
  if (success) {
    print('引擎加载成功');
  } else {
    print('引擎加载失败');
  }
});
2.2.2 openFile 方法
void openFile(int platformViewId, String filePath, Function(bool)? onOpen)

参数说明:

参数名 类型 必填 说明
platformViewId int 平台视图 ID,由 PlatformView 创建时生成
filePath String 本地文件路径
onOpen Function(bool)? 文件打开回调,true 表示成功,false 表示失败(不支持的文件类型)

功能说明:

在平台视图创建完成后,调用此方法打开指定文件。通过 MethodChannel 与原生端通信。

使用场景:

  • 内部使用,通常不需要直接调用
  • 自定义文件查看器时可能需要

使用示例:

// 通常由 FileReaderView 内部调用
FileReader.instance.openFile(
  platformViewId,
  '/path/to/file.txt',
  (success) {
    if (!success) {
      print('不支持的文件类型');
    }
  },
);

2.3 FileReaderState 枚举

enum FileReaderState {
  LOADING_ENGINE,        // 正在加载引擎
  ENGINE_LOAD_SUCCESS,   // 引擎加载成功
  ENGINE_LOAD_FAIL,      // 引擎加载失败(仅 Android,iOS 忽略)
  UNSUPPORT_FILE,        // 不支持的文件类型
  FILE_NOT_FOUND,        // 文件不存在
}

功能说明:

表示文件读取器的当前状态,FileReaderView 根据此状态显示不同的界面。

状态说明:

状态 说明 默认显示
LOADING_ENGINE 正在加载原生引擎 CupertinoActivityIndicator 或自定义 loadingWidget
ENGINE_LOAD_SUCCESS 引擎加载成功,准备打开文件 显示文件内容
ENGINE_LOAD_FAIL 引擎加载失败 提示"引擎加载失败,请退出重试"
UNSUPPORT_FILE 文件类型不支持 提示"不支持打开 xx 类型的文件"或自定义 unSupportFileWidget
FILE_NOT_FOUND 文件路径不存在 提示"文件不存在"

3. 使用示例

3.1 基础文件查看

import 'package:flutter/material.dart';
import 'package:flutter_filereader/flutter_filereader.dart';

class FileReaderPage extends StatelessWidget {
  final String filePath;

  const FileReaderPage({Key? key, required this.filePath}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('文件查看'),
      ),
      body: FileReaderView(
        filePath: filePath,
      ),
    );
  }
}

3.2 自定义加载提示

FileReaderView(
  filePath: filePath,
  loadingWidget: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const CircularProgressIndicator(),
        const SizedBox(height: 16),
        Text(
          '正在加载文档...',
          style: TextStyle(
            fontSize: 16,
            color: Colors.grey.shade600,
          ),
        ),
      ],
    ),
  ),
)

3.3 文件类型判断

String getFileType(String filePath) {
  if (filePath.isEmpty) return '';
  final index = filePath.lastIndexOf('.');
  if (index <= -1) return '';
  return filePath.substring(index + 1).toLowerCase();
}

// 使用
final fileType = getFileType('/path/to/document.pdf');
print(fileType); // 输出: pdf

3.4 从 Assets 复制文件到本地

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';

Future<String> copyAssetToLocal(String assetPath, String fileName) async {
  final dir = await getApplicationDocumentsDirectory();
  final file = File('${dir.path}/$fileName');
  
  if (!await file.exists()) {
    await file.create(recursive: true);
    final data = await rootBundle.load(assetPath);
    await file.writeAsBytes(data.buffer.asUint8List());
  }
  
  return file.path;
}

4. 完整代码

在这里插入图片描述

4.1 企业文档管理中心

这是一个应用级别的完整示例,实现了一个"企业文档管理中心"应用。用户可以浏览公司文档、查看文档详情、在线阅读文档,并支持文档分类和搜索功能。

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_filereader/flutter_filereader.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '企业文档管理中心',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const DocumentCenterScreen(),
    );
  }
}

class DocumentItem {
  final String title;
  final String description;
  final String content;
  final String category;
  final String fileType;
  final DateTime updateTime;
  final String size;

  const DocumentItem({
    required this.title,
    required this.description,
    required this.content,
    required this.category,
    required this.fileType,
    required this.updateTime,
    required this.size,
  });
}

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

  
  State<DocumentCenterScreen> createState() => _DocumentCenterScreenState();
}

class _DocumentCenterScreenState extends State<DocumentCenterScreen>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  String _searchQuery = '';

  final List<DocumentItem> _documents = [
    DocumentItem(
      title: '员工手册',
      description: '公司员工规章制度、行为准则、福利待遇等',
      content: '''员工手册

第一章 总则
第一条 为规范公司管理,维护员工合法权益,制定本手册。
第二条 本手册适用于公司全体员工。

第二章 员工权利与义务
第三条 员工享有平等就业、取得劳动报酬、休息休假等权利。
第四条 员工应当遵守公司规章制度,履行工作职责。

第三章 考勤管理
第五条 工作时间为每周一至周五,上午9:00-12:00,下午13:00-18:00。
第六条 员工应当按时上下班,不得迟到早退。

第四章 薪酬福利
第七条 公司实行岗位绩效工资制。
第八条 公司为员工缴纳社会保险和住房公积金。

第五章 附则
第九条 本手册自发布之日起施行。
第十条 本手册由公司人力资源部负责解释。''',
      category: '规章制度',
      fileType: 'txt',
      updateTime: DateTime(2024, 3, 15),
      size: '128 KB',
    ),
    DocumentItem(
      title: '请假流程说明',
      description: '各类请假申请流程和审批流程详解',
      content: '''请假流程说明

一、请假类型
1. 事假:因私事需要请假
2. 病假:因病需要请假,需提供医院证明
3. 年假:根据工作年限享受的带薪假期
4. 婚假:结婚时享受的假期
5. 产假:女职工生育时享受的假期

二、请假流程
1. 员工在OA系统提交请假申请
2. 直接主管审批(1个工作日内)
3. 部门负责人审批(1个工作日内)
4. 人力资源部备案

三、注意事项
1. 请假需提前申请,不得事后补请
2. 病假需提供医院诊断证明
3. 年假需提前一周申请
4. 请假期间保持通讯畅通''',
      category: '规章制度',
      fileType: 'txt',
      updateTime: DateTime(2024, 2, 20),
      size: '64 KB',
    ),
    DocumentItem(
      title: '2024年Q1销售报告',
      description: '第一季度销售业绩、市场分析和下季度计划',
      content: '''2024年第一季度销售报告

一、销售业绩概览
1. 总销售额:1500万元,同比增长25%
2. 新客户数量:120家,同比增长30%
3. 客户满意度:95%,同比提升5个百分点

二、区域销售情况
1. 华东地区:600万元,占比40%
2. 华南地区:450万元,占比30%
3. 华北地区:300万元,占比20%
4. 其他地区:150万元,占比10%

三、产品线销售
1. 产品A:800万元,占比53%
2. 产品B:500万元,占比33%
3. 产品C:200万元,占比14%

四、下季度计划
1. 目标销售额:1800万元
2. 重点开拓华北市场
3. 推出新产品D
4. 增加销售团队人员10名''',
      category: '业务报告',
      fileType: 'txt',
      updateTime: DateTime(2024, 4, 5),
      size: '256 KB',
    ),
    DocumentItem(
      title: '产品使用指南',
      description: '产品功能介绍、操作步骤和常见问题解答',
      content: '''产品使用指南

一、产品简介
本产品是一款智能办公管理系统,帮助企业提高办公效率。

二、主要功能
1. 文档管理:上传、下载、分享文档
2. 审批流程:在线审批、电子签名
3. 日程管理:日程安排、会议预约
4. 通讯录:企业通讯录、部门架构

三、操作步骤
1. 登录系统:使用工号和密码登录
2. 选择功能模块:点击左侧导航栏
3. 执行操作:按照页面提示完成操作
4. 保存提交:确认信息无误后提交

四、常见问题
Q1: 忘记密码怎么办?
A: 点击"忘记密码",通过邮箱重置。

Q2: 如何修改个人信息?
A: 进入"个人中心",点击"编辑资料"。

Q3: 如何联系技术支持?
A: 拨打技术支持热线:400-xxx-xxxx''',
      category: '产品文档',
      fileType: 'txt',
      updateTime: DateTime(2024, 3, 28),
      size: '512 KB',
    ),
    DocumentItem(
      title: '新员工入职指南',
      description: '入职流程、办公环境介绍、常用工具使用指南',
      content: '''新员工入职指南

欢迎加入公司!

一、入职流程
1. 报到登记:携带身份证、学历证明到人力资源部报到
2. 签订合同:签订劳动合同和保密协议
3. 领取物品:领取工牌、办公用品、电脑等
4. 系统开通:开通邮箱、OA系统账号

二、办公环境
1. 办公区域:A栋3楼,工位号见工位图
2. 餐厅:B栋1楼,工作时间7:00-19:00
3. 会议室:提前在OA系统预约
4. 停车场:地下车库,凭工牌进出

三、常用工具
1. 企业邮箱:name@company.com
2. OA系统:http://oa.company.com
3. 即时通讯:企业微信
4. 文档共享:公司网盘

四、联系方式
人力资源部:分机8001
IT支持:分机8002
行政服务:分机8003''',
      category: '培训资料',
      fileType: 'txt',
      updateTime: DateTime(2024, 1, 10),
      size: '192 KB',
    ),
    DocumentItem(
      title: '信息安全规范',
      description: '数据安全、网络安全、密码管理等安全规范',
      content: '''信息安全规范

一、总则
为保障公司信息安全,制定本规范。

二、密码管理
1. 密码长度不少于8位
2. 密码包含大小写字母、数字和特殊字符
3. 密码每90天更换一次
4. 不得共享密码

三、数据安全
1. 重要数据定期备份
2. 敏感数据加密存储
3. 未经授权不得导出数据
4. 离职时交回所有数据资料

四、网络安全
1. 不得访问非法网站
2. 不得下载不明软件
3. 发现安全事件立即报告
4. 定期更新杀毒软件

五、设备安全
1. 电脑设置自动锁屏(5分钟)
2. 离开座位时锁定屏幕
3. 不得私自安装外设
4. 设备故障联系IT部门

六、违规处理
违反本规范的,视情节轻重给予警告、记过、解除劳动合同等处理。''',
      category: '规章制度',
      fileType: 'txt',
      updateTime: DateTime(2024, 2, 15),
      size: '96 KB',
    ),
  ];

  
  void initState() {
    super.initState();
    final categories = _documents.map((d) => d.category).toSet().toList();
    _tabController = TabController(length: categories.length, vsync: this);
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  List<DocumentItem> get _filteredDocuments {
    var documents = _documents;
    if (_searchQuery.isNotEmpty) {
      documents = documents
          .where((d) =>
              d.title.contains(_searchQuery) ||
              d.description.contains(_searchQuery))
          .toList();
    }
    return documents;
  }

  List<String> get _categories {
    return _documents.map((d) => d.category).toSet().toList();
  }

  IconData _getFileIcon(String fileType) {
    switch (fileType.toLowerCase()) {
      case 'txt':
        return Icons.description;
      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;
      default:
        return Icons.insert_drive_file;
    }
  }

  Color _getFileColor(String fileType) {
    switch (fileType.toLowerCase()) {
      case 'txt':
        return Colors.blue;
      case 'pdf':
        return Colors.red;
      case 'doc':
      case 'docx':
        return Colors.blue.shade700;
      case 'xls':
      case 'xlsx':
        return Colors.green;
      case 'ppt':
      case 'pptx':
        return Colors.orange;
      default:
        return Colors.grey;
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('企业文档中心'),
        elevation: 0,
        backgroundColor: Colors.teal.shade700,
        foregroundColor: Colors.white,
      ),
      body: Column(
        children: [
          Container(
            color: Colors.white,
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: TextField(
              decoration: InputDecoration(
                hintText: '搜索文档...',
                prefixIcon: const Icon(Icons.search),
                suffixIcon: _searchQuery.isNotEmpty
                    ? IconButton(
                        icon: const Icon(Icons.clear),
                        onPressed: () {
                          setState(() {
                            _searchQuery = '';
                          });
                        },
                      )
                    : null,
                filled: true,
                fillColor: Colors.grey.shade100,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(24),
                  borderSide: BorderSide.none,
                ),
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 8,
                ),
              ),
              onChanged: (value) {
                setState(() {
                  _searchQuery = value;
                });
              },
            ),
          ),
          TabBar(
            controller: _tabController,
            isScrollable: true,
            tabAlignment: TabAlignment.start,
            tabs: _categories
                .map((category) => Tab(text: category))
                .toList(),
          ),
          Expanded(
            child: TabBarView(
              controller: _tabController,
              children: _categories.map((category) {
                final categoryDocs = _filteredDocuments
                    .where((d) => d.category == category)
                    .toList();
                return _buildDocumentList(categoryDocs);
              }).toList(),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildDocumentList(List<DocumentItem> documents) {
    if (documents.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.folder_off,
              size: 80,
              color: Colors.grey.shade400,
            ),
            const SizedBox(height: 16),
            Text(
              '没有找到相关文档',
              style: TextStyle(
                fontSize: 16,
                color: Colors.grey.shade600,
              ),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: documents.length,
      itemBuilder: (context, index) {
        final doc = documents[index];
        return _buildDocumentCard(doc);
      },
    );
  }

  Widget _buildDocumentCard(DocumentItem doc) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      elevation: 3,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: InkWell(
        borderRadius: BorderRadius.circular(12),
        onTap: () async {
          final localPath = await _createLocalFile(doc);
          if (localPath != null) {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => DocumentViewerScreen(
                  filePath: localPath,
                  title: doc.title,
                ),
              ),
            );
          } else {
            if (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text('文件加载失败'),
                  backgroundColor: Colors.red,
                ),
              );
            }
          }
        },
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: _getFileColor(doc.fileType).withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(
                  _getFileIcon(doc.fileType),
                  color: _getFileColor(doc.fileType),
                  size: 32,
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      doc.title,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 4),
                    Text(
                      doc.description,
                      style: TextStyle(
                        fontSize: 13,
                        color: Colors.grey.shade600,
                      ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 12,
                      runSpacing: 4,
                      children: [
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Icon(Icons.access_time, size: 14, color: Colors.grey.shade500),
                            const SizedBox(width: 4),
                            Text(
                              '${doc.updateTime.year}-${doc.updateTime.month.toString().padLeft(2, '0')}-${doc.updateTime.day.toString().padLeft(2, '0')}',
                              style: TextStyle(
                                fontSize: 12,
                                color: Colors.grey.shade500,
                              ),
                            ),
                          ],
                        ),
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Icon(Icons.storage, size: 14, color: Colors.grey.shade500),
                            const SizedBox(width: 4),
                            Text(
                              doc.size,
                              style: TextStyle(
                                fontSize: 12,
                                color: Colors.grey.shade500,
                              ),
                            ),
                          ],
                        ),
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 2,
                          ),
                          decoration: BoxDecoration(
                            color: Colors.teal.shade50,
                            borderRadius: BorderRadius.circular(8),
                            border: Border.all(color: Colors.teal.shade200),
                          ),
                          child: Text(
                            doc.fileType.toUpperCase(),
                            style: TextStyle(
                              fontSize: 11,
                              fontWeight: FontWeight.bold,
                              color: Colors.teal.shade700,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              Icon(
                Icons.arrow_forward_ios,
                size: 16,
                color: Colors.grey.shade400,
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<String?> _createLocalFile(DocumentItem doc) async {
    try {
      final dir = await getApplicationDocumentsDirectory();
      final safeFileName = doc.title.replaceAll(RegExp(r'[^\w\s\u4e00-\u9fff]'), '_');
      final file = File('${dir.path}/documents/$safeFileName.txt');

      if (!await file.exists()) {
        await file.create(recursive: true);
        await file.writeAsString(doc.content);
      }

      return file.path;
    } catch (e) {
      debugPrint('创建文件失败: $e');
      return null;
    }
  }
}

class DocumentViewerScreen extends StatelessWidget {
  final String filePath;
  final String title;

  const DocumentViewerScreen({
    super.key,
    required this.filePath,
    required this.title,
  });

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        elevation: 0,
        actions: [
          IconButton(
            icon: const Icon(Icons.info_outline),
            onPressed: () {
              showDialog(
                context: context,
                builder: (context) => AlertDialog(
                  title: const Text('文档信息'),
                  content: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('文件路径: $filePath'),
                      const SizedBox(height: 8),
                      Text('文件名: ${filePath.split('/').last}'),
                    ],
                  ),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.of(context).pop(),
                      child: const Text('关闭'),
                    ),
                  ],
                ),
              );
            },
          ),
        ],
      ),
      body: FileReaderView(
        filePath: filePath,
        loadingWidget: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const CircularProgressIndicator(),
              const SizedBox(height: 16),
              Text(
                '正在加载文档...',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey.shade600,
                ),
              ),
            ],
          ),
        ),
        unSupportFileWidget: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.error_outline,
                size: 64,
                color: Colors.orange.shade400,
              ),
              const SizedBox(height: 16),
              Text(
                '暂不支持此文件格式',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey.shade700,
                ),
              ),
              const SizedBox(height: 8),
              Text(
                '当前平台仅支持 TXT 格式',
                style: TextStyle(
                  fontSize: 14,
                  color: Colors.grey.shade500,
                ),
              ),
            ],
          ),
        ),
        openSuccess: (success) {
          debugPrint('文件打开${success ? '成功' : '失败'}');
        },
      ),
    );
  }
}

4.2 依赖配置

pubspec.yaml 中添加以下依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_filereader:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_flutter_filereader.git
  path_provider:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages.git
      path: packages/path_provider/path_provider
      ref: master

4.3 代码详解

4.3.1 文档数据模型
class DocumentItem {
  final String title;           // 文档标题
  final String description;     // 文档描述
  final String content;         // 文档内容
  final String category;        // 分类
  final String fileType;        // 文件类型
  final DateTime updateTime;    // 更新时间
  final String size;            // 文件大小
}

这个数据模型包含了文档的所有必要信息,用于在列表中展示和跳转查看。文档内容直接存储在 content 字段中,通过 path_provider 创建本地文件。

4.3.2 文件类型图标映射
IconData _getFileIcon(String fileType) {
  switch (fileType.toLowerCase()) {
    case 'txt':
      return Icons.description;
    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;
    default:
      return Icons.insert_drive_file;
  }
}

根据文件类型返回对应的图标,让用户一眼识别文件格式。

4.3.3 文件类型颜色映射
Color _getFileColor(String fileType) {
  switch (fileType.toLowerCase()) {
    case 'txt':
      return Colors.blue;
    case 'pdf':
      return Colors.red;
    case 'doc':
    case 'docx':
      return Colors.blue.shade700;
    case 'xls':
    case 'xlsx':
      return Colors.green;
    case 'ppt':
    case 'pptx':
      return Colors.orange;
    default:
      return Colors.grey;
  }
}

不同文件类型使用不同颜色,增强视觉区分度。

4.3.4 创建本地文件
Future<String?> _createLocalFile(DocumentItem doc) async {
  try {
    final dir = await getApplicationDocumentsDirectory();
    final safeFileName = doc.title.replaceAll(RegExp(r'[^\w\s\u4e00-\u9fff]'), '_');
    final file = File('${dir.path}/documents/$safeFileName.txt');

    if (!await file.exists()) {
      await file.create(recursive: true);
      await file.writeAsString(doc.content);
    }

    return file.path;
  } catch (e) {
    debugPrint('创建文件失败: $e');
    return null;
  }
}

这个函数负责将文档内容创建到本地目录,使用 path_provider 获取应用文档目录,然后使用 File.writeAsString() 写入内容。FileReaderView 只能打开本地文件,所以需要先将内容写入本地。

4.3.5 文档查看页面
class DocumentViewerScreen extends StatelessWidget {
  final String filePath;
  final String title;

  const DocumentViewerScreen({
    super.key,
    required this.filePath,
    required this.title,
  });

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        elevation: 0,
        actions: [
          IconButton(
            icon: const Icon(Icons.info_outline),
            onPressed: () {
              // 显示文档信息对话框
            },
          ),
        ],
      ),
      body: FileReaderView(
        filePath: filePath,
        loadingWidget: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const CircularProgressIndicator(),
              const SizedBox(height: 16),
              Text('正在加载文档...'),
            ],
          ),
        ),
        unSupportFileWidget: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.error_outline, size: 64, color: Colors.orange.shade400),
              const SizedBox(height: 16),
              Text('暂不支持此文件格式'),
              const SizedBox(height: 8),
              Text('当前平台仅支持 TXT 格式'),
            ],
          ),
        ),
        openSuccess: (success) {
          debugPrint('文件打开${success ? '成功' : '失败'}');
        },
      ),
    );
  }
}

文档查看页面使用 FileReaderView 组件,并自定义了加载中和不支持文件的提示界面。

5. 常见问题解答

Q1: 如何使用 FileReaderView 打开本地文件?

解决方案: 只需传入本地文件的绝对路径:

FileReaderView(
  filePath: '/data/storage/el2/base/documents/file.txt',
)

注意: 文件路径必须是本地绝对路径,不支持网络 URL。

Q2: 如何使用 path_provider 创建本地文件?

解决方案: 使用 getApplicationDocumentsDirectory() 获取文档目录,然后创建文件:

import 'dart:io';
import 'package:path_provider/path_provider.dart';

Future<String?> createLocalFile(String fileName, String content) async {
  final dir = await getApplicationDocumentsDirectory();
  final file = File('${dir.path}/documents/$fileName.txt');
  
  if (!await file.exists()) {
    await file.create(recursive: true);
    await file.writeAsString(content);
  }
  
  return file.path;
}

Q3: 如何自定义加载中的提示界面?

解决方案: 使用 loadingWidget 参数:

FileReaderView(
  filePath: filePath,
  loadingWidget: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const CircularProgressIndicator(),
        const SizedBox(height: 16),
        Text(
          '正在加载文档...',
          style: TextStyle(
            fontSize: 16,
            color: Colors.grey.shade600,
          ),
        ),
      ],
    ),
  ),
)

Q4: 如何自定义不支持文件的提示界面?

解决方案: 使用 unSupportFileWidget 参数:

FileReaderView(
  filePath: filePath,
  unSupportFileWidget: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(
          Icons.error_outline,
          size: 64,
          color: Colors.orange.shade400,
        ),
        const SizedBox(height: 16),
        Text(
          '暂不支持此文件格式',
          style: TextStyle(
            fontSize: 16,
            color: Colors.grey.shade700,
          ),
        ),
      ],
    ),
  ),
)

Q5: 如何监听文件打开状态?

解决方案: 使用 openSuccess 回调:

FileReaderView(
  filePath: filePath,
  openSuccess: (success) {
    if (success) {
      print('文件打开成功');
    } else {
      print('文件打开失败,可能是不支持的文件类型');
    }
  },
)

Q6: OpenHarmony 支持哪些文件格式?

解决方案: 当前 OpenHarmony 版本仅支持 TXT 格式文件。

平台 支持格式
OpenHarmony TXT
Android DOCX、DOC、XLSX、XLS、PPTX、PPT、PDF、TXT
iOS DOCX、DOC、XLSX、XLS、PPTX、PPT、PDF、TXT、JPG、JPEG、PNG

Q7: TXT 文件显示乱码怎么办?

解决方案: 将 TXT 文件编码改为 GBK 编码:

  1. 使用记事本打开 TXT 文件
  2. 点击"文件" -> “另存为”
  3. 在"编码"下拉框中选择 “ANSI”(Windows 默认 ANSI 即为 GBK)
  4. 保存文件

Q8: 如何检查文件是否存在?

解决方案: 使用 File.exists() 方法:

import 'dart:io';

Future<bool> checkFileExists(String filePath) async {
  return await File(filePath).exists();
}

// 使用
final exists = await checkFileExists('/path/to/file.txt');
if (exists) {
  print('文件存在');
} else {
  print('文件不存在');
}

Q9: 如何获取应用文档目录?

解决方案: 使用 path_provider 库:

import 'package:path_provider/path_provider.dart';

Future<String> getDocumentDirectory() async {
  final dir = await getApplicationDocumentsDirectory();
  return dir.path;
}

// 使用
final docDir = await getDocumentDirectory();
print('文档目录: $docDir');

Q10: 如何释放 FileReaderView 占用的资源?

解决方案: FileReaderView 会自动管理资源,无需手动释放。当页面销毁时,资源会自动释放。


void dispose() {
  // FileReaderView 会自动释放资源
  super.dispose();
}

Q11: flutter_filereader 在 OpenHarmony 上有哪些已知问题?

目前 flutter_filereader 库在 OpenHarmony 平台上运行正常,需要注意:

  1. 支持格式有限:当前仅支持 TXT 格式,其他格式(PDF、Word、Excel 等)暂不支持
  2. 引擎加载时间:首次打开文件时会异步加载原生引擎,需要一定时间
  3. 文件编码:TXT 文件需要使用 GBK 编码,否则可能显示乱码
  4. 权限要求:需要相应的文件访问权限

6. 注意事项

  1. OpenHarmony 支持格式:当前 OpenHarmony 版本仅支持 TXT 格式文件,其他格式(PDF、Word、Excel 等)暂不支持
  2. 文件路径filePath 必须是本地绝对路径,不支持网络 URL
  3. 本地文件:使用 path_provider 获取应用文档目录,将文件内容写入本地后再使用 FileReaderView 打开
  4. TXT 编码:如果 TXT 文件显示乱码,请将文件编码改为 GBK
  5. 引擎加载:首次打开文件时会异步加载原生引擎,需要一定时间
  6. 权限要求:Android 端需要存储权限,OpenHarmony 端需要相应的文件访问权限
  7. 不支持模拟器:Android 端不支持在模拟器上运行(由于 X5 内核限制)
  8. Google Play 限制:Android 端基于腾讯 X5 内核,不支持发布到 Google Play

7. 总结

flutter_filereader 是一个实用的文件查看器库,在 OpenHarmony 环境下可以运行,当前支持 TXT 格式文件。通过本文章的学习,我们掌握了:

  • FileReaderView 组件的使用方法和参数配置
  • FileReader 单例类的 engineLoadStatusopenFile 方法
  • FileReaderState 枚举表示的各种状态
  • 自定义加载中和不支持文件的提示界面
  • 从 Assets 复制文件到本地的方法
  • 完整的"企业文档管理中心"应用实现

通过 flutter_filereader,你可以轻松实现办公文档查看应用、文件管理器预览、在线教育平台课件查看、企业 OA 系统附件查看等场景,为 OpenHarmony 应用增添强大的文件查看能力。

Logo

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

更多推荐