最近在搞一个基于 Flutter for OpenHarmony 的身体健康状况记录 App,功能做到最后卡在了一个看似简单、实则到处是坑的环节——数据导出。原本以为就是把数据库里的记录写成文件扔到本地,结果在 OpenHarmony 的权限模型、路径体系、Flutter 插件兼容性上连续踩了好几天。这篇文章把我整个排查过程和最终落地方案完整记录下来,包括数据结构设计、CSV 导出、JSON 导出、文件分享、平台通道的接入,以及一堆只有真机调试才能发现的细节。如果你也在 OpenHarmony 上做 Flutter 应用,或者正在给你的健康类 App 补数据导出能力,这篇应该能帮你少走不少弯路。
1. 数据导出功能先想清楚再动手
1.1 没有导出功能的健康记录 App 是半个残废
身体健康状况记录类的 App,核心价值在于数据积累。用户每天记录的体重、血压、心率、睡眠时长、用药情况,沉淀下来才是真正有价值的东西。但如果这些数据只能躺在 App 的数据库里,用户换手机、卸载应用、或者想拿给医生看的时候,数据就彻底被困住了。这也是我在项目初期就坚持要把导出功能做进 MVP 的原因——数据导出不是锦上添花,而是数据主权的基本保障。
从用户场景来看,导出功能有几个刚需出口:一是备份,用户定期把记录导出成文件保存;二是分享,把一段时间内的健康数据发给医生或者家人;三是在其他工具里做二次分析,比如导入 Excel 做趋势图。这三个场景对导出格式的要求不太一样,备份和传输要求通用性,二次分析要求结构化程度高。所以我的方案里没有只做一个格式,而是同时支持 JSON 和 CSV 两种,后续如果有需要再把 Excel 格式加进去。
在 OpenHarmony 上做这件事,比 Android 上要复杂一些。Flutter 本身在 OpenHarmony 上的适配已经走过了"能跑起来"的阶段,但很多常用插件还没有完全移植过来。到写这篇文章为止,path_provider 在 OpenHarmony 上可以做一些基础路径获取,但行为和一些 Android 版本不太一致;share_plus 在某些版本上能弹分享面板,但预览和 MIME 类型的处理还有不少问题。这意味着你不能直接照搬网上 Android 的教程代码,需要自己补一层适配。
1.2 导出方案选型:先想清楚要导出什么
很多人在做导出功能时,第一步就跑偏了,直接打开代码开始写文件的读写逻辑。我建议先花半小时回答一个问题:导出的内容到底是什么?是当前列表页展示的那几条记录,还是一个用户的所有健康档案?
我的表结构里,核心表叫 health_records,每一条记录包含记录时间、体重、体脂率、收缩压、舒张压、心率、睡眠时长、症状备注等字段。此外还有用户的个人设置项,比如身高、出生年份、性别、目标体重等。导出时如果只导记录表,用户拿到的是一堆数字,缺少解读上下文;如果把个人设置也导出来,文件就更有复用价值。所以我把导出内容拆成了两层:record 部分是完整的记录数组,meta 部分是用户档案和导出元信息,比如导出时间、App 版本、导出工具标识。
这样的设计还有一个好处,为后面做数据导回预留了伏笔。只要导出的格式是"完整档案 + 记录流",将来用户导入时就能完整恢复整个 App 的状态,不仅仅是记录列表。健康记录类 App 的数据迁移功能,很多厂商做得并不好,但对我们这种独立开发场景来说,一个可靠的 JSON 文件就能解决绝大部分迁移需求。
格式选择上,JSON 适合程序之间交换数据,CSV 适合人类和电子表格工具。考虑到分享给医生看这类场景,CSV 几乎必然是第一选择,因为医生不可能为了看你的数据去装一个 App,但他们几乎都能打开 Excel。而 CSV 的坑也很多,编码、分隔符、字段内含逗号、布尔值表示方式都会影响最终体验,这些在第三章我会逐个讲。
1.3 OpenHarmony 上 Flutter 的适配现状
开始写代码之前,建议先确认你的 Flutter SDK 是哪个分支。我使用的是官方为 OpenHarmony 维护的 Flutter 版本,版本号和 Android 侧的 Flutter 不完全同步。早期用错版本会出现各种诡异问题,比如 Dart 侧调 PlatformChannel 时 native 方法一直找不到,或者打包出来的 hap 直接白屏。
OpenHarmony 应用模型是以 Stage 模型为主,Ability 的概念和 Android 的 Activity 不一样,权限申请方式也有差异。对 Flutter 应用来说,纯 Dart 层代码是可以跨平台的,但一旦涉及文件存储、系统分享、权限申请这些需要访问系统能力的地方,就必须清楚你的代码最终跑在哪个平台上。
我测试时用了两台设备,一台是运行 OpenHarmony 的 Dayu 开发板,一台是模拟器。这里要特别提醒,模拟器的文件系统行为跟真机差异很大,尤其是沙箱路径和公共目录访问权限,模拟器往往会放得很宽,真机上就立刻现原形。开发阶段如果条件允许,尽量尽早用真机跑一跑导出相关代码,不要等到最后打包才去做真机验证。
2. 核心细节:数据模型、编码和路径
2.1 数据模型设计:导出的不只是一堆数字
导出功能的代码结构其实不复杂,核心是三个环节:从数据库取出数据、序列化成目标格式、写入文件并触发分享。如果数据库用的是 drift 或者 sqflite,取数逻辑可以复用已有的 repository 层。我这里用的是 sqflite,定义了一个简单的 HealthRepository,封装了按时间段查询记录的方法。
我建议在 repository 层就完成数据到导出模型对象的映射,不要在 UI 层做。因为导出往往需要"全量数据"或者"某段时间的数据",而 UI 层只需要列表页范围内的数据,两者的查询条件不一样。如果 UI 层复用列表页的模型去拼导出数据,十有八九会漏字段。
字段选择上要注意:把数据库中所有字段都导出一份是最省事的,但有几个字段需要转换。比如时间戳我存的是毫秒值,导出时如果直接写毫秒数字,用户看 CSV 根本看不懂;而 JSON 格式里保留原始毫秒值反而更适合程序解析。所以我的方案是:JSON 导出保留原始值,CSV 导出把时间戳格式化成"2024-06-15 08:30"这样的可读格式。
如果你记录里有症状备注这种自由文本,CSV 导出时千万小心。文本里可能会带逗号、换行符、双引号,直接拼进 CSV 会导致列错位、行数错误。正确做法是遵循 RFC 4180 规范,包含特殊字符的字段用双引号包裹,并把字段内部的双引号用两个双引号转义。我在下文会给出完整的处理函数,不建议用那种"简单 join 逗号"的写法,导出功能上线后被用户反馈 Excel 打开乱掉,基本都是这个原因。
2.2 中文乱码的根因与 BOM 头处理
CSV 导出的第一个坑不是数据,而是编码。Windows 上的 Excel 默认打开 CSV 时,如果你没有在文件头部加上 UTF-8 BOM,中文非常容易乱码。实测下来,OpenHarmony 上用 File.writeAsString 写 UTF-8 文件后,再用 WPS 打开,不带 BOM 的时候中文注释是乱码,带 BOM 就能正常识别。这个现象在 Mac 的 Numbers 里影响不大,但在 Windows Excel 用户那里几乎是必现。
解决办法是写文件时在字符串最前面加上 \uFEFF 字符。注意 BOM 必须加在第一个可见字符之前,不能有任何空格或换行在前面,否则 BOM 不在文件头,Excel 就识别不到了。
Dart 侧的写法很简单,一行代码就够。这个细节不写在代码里,最后测试阶段很容易漏掉。最好在生成 CSV 字符串的方法入口处统一加 BOM,不要拿到 UI 层再加,免得某些调用方漏掉。我前前后后因为这个被测试同事追着反馈了两次,后来直接在 CSV 导出工具类的构造函数里强制加前缀,才彻底杜绝。
另外,CSV 的分隔符问题也要提一下。我默认用的是逗号,因为这是最常见的标准。但法语地区、葡萄牙语地区的一些 Excel 版本默认把分号当作分隔符,导出的 CSV 打开后所有字段挤在一列。如果你打算上架到海外,建议在设置项里允许用户选择分隔符,或者做一个简单的首行检测。对国内场景来说,逗号加 BOM 基本够用。
2.3 文件存哪里:沙箱、公共目录与媒体库
OpenHarmony 的文件存储权限模型比 Android 更严格,理解这一点能从源头上避免大批 bug。Flutter 的 path_provider 在 OpenHarmony 上可以拿到 getApplicationDocumentsDirectory 和 getExternalStorageDirectory 这类路径,但请记住:这些目录大多数处于应用沙箱内,导出后用户通过文件管理器根本看不到。
用户的直观认知是"导出的文件应该出现在 Download 或者 Documents 里,我能用文件管理器找到它"。如果文件只在应用私有目录,分享给医生的操作就很难进行,因为你在文件管理器里找不到入口,只能依赖系统分享面板。所以,如果你只是做一个"导出并保存到本地"的按钮,大概率会被用户投诉"文件保存成功但找不到"。
在 OpenHarmony 上我最终的策略是:优先调用系统分享面板,把文件从应用沙箱临时目录分享出去。如果用户选择保存,我们把它写到应用的公共文件目录(对应媒体库中 App 专属文件区域),然后通过媒体库通知应用让文件在文件管理器可见。这个过程需要 OpenHarmony 的 Media Library 能力,需要用 MethodChannel 到 Native 侧去调用。
如果你不想做那么深,最低底线是:导出成功后,在应用内做一个"导出列表"页面,把生成的每个文件都展示出来,用户可以通过列表项进入分享操作。这个方案纯 Flutter 就能实现,兼容性最好,用户体验也能接受。我自己的正式版里两个方案都做了:导出列表页作为兜底,系统分享面板作为主入口。
3. 实操过程:JSON 和 CSV 导出的完整实现
3.1 导出 JSON:最快捷的 MVP 方案
JSON 导出是最容易实现的一种,Dart 对 JSON 的原生支持足够好,不需要引入额外依赖。核心代码也就两步:把数据对象构造成 Map,再用 jsonEncode 序列化,最后写入文件。
我先定义了一个 ExportModel 对象,它有两个成员:meta 和 records。meta 里放导出时间、App 版本、用户基本档案;records 是从数据库查询出来的原始记录映射后的 List。序列化时用 toJson 方法把对象转成 Map,避免直接拿 entity 去 jsonEncode 导致字段名和预期不一致。
文件写入的关键是路径。我用 path_provider 的 getTemporaryDirectory 拿到应用临时目录,再拼上文件名字符串。这样做的原因是分享面板通常需要一个 file:// 或者 content:// 的 URI 来源,临时目录里的文件可以直接被分享组件读取。如果你希望同时保留归档,可以在导出完成后把临时文件复制到公共目录,然后再发一次媒体库通知。
dateTime 的格式化需要手动处理。我建议用固定的格式,比如 yyyyMMdd_HHmmss,拼在文件名里,例如 health_export_20250306_143300.json。这样同一秒内如果用户反复导出,文件名也不会冲突。如果用时间戳的毫秒值做文件名,虽然不会重,但对人类不友好,后期排查文件时很难定位。
Future<String> exportToJson(List<HealthRecord> records, UserProfile profile) async { final dir = await getTemporaryDirectory(); final timestamp = _formatTimestamp(DateTime.now()); final filePath = '${dir.path}/health_export_$timestamp.json'; final payload = ExportPayload( meta: ExportMeta( exportedAt: DateTime.now(), appVersion: '1.2.0', profile: profile, ), records: records, ); final content = const JsonEncoder.withIndent(' ').convert(payload.toJson()); final file = File(filePath); await file.writeAsString(content, flush: true); return filePath; }这里我用带缩进的 JsonEncoder,而不是 jsonEncode,因为输出文件是人类要看的,压缩成一行字符串在移动端查看体验极差。带缩进的文件体积会大一点,但健康记录的体量通常很小,完全无所谓。
3.2 导出 CSV:Excel 可读才是刚需
CSV 的导出代码比 JSON 多一点,主要多在处理转义和加 BOM。我实际项目里写了一个 CsvWriter 工具类,专门处理字段转义,避免把所有逻辑都堆在业务代码里,方便复用。
CSV 转义的规则是:先判断字段中是否包含逗号、双引号、换行符中的任何一个。如果包含,就需要用双引号把整个字段包起来,并将字段内部的每个双引号替换成两个双引号。如果字段不包含特殊字符,直接原样输出。
class CsvWriter { static String escape(String field) { if (field.contains(',') || field.contains('"') || field.contains('\n') || field.contains('\r')) { return '"${field.replaceAll('"', '""')}"'; } return field; } }表头建议固定写英文,或者用中文表头加 BOM 处理。我这里的健康记录表头包括:记录时间、体重、体脂率、收缩压、舒张压、心率、睡眠时长、症状备注。这样导出后直接用 Excel 打开,每一列含义一目了然。如果你想让机器更容易处理,可以在第一行加一个"CSV 导出于 xx 时间"的注释行,但这样会影响某些 CSV 解析库的兼容性,所以我没加。
生成 CSV 字符串时用 StringBuffer 拼接,每一行以换行符结束。注意用 \r\n 还是 \n:Windows Excel 对 CSV 的行结束符容忍度较高,但为了保险我使用 \r\n。Dart 的 File.writeAsString 默认不会做换行转换,所以你在字符串里写什么,文件里就是什么。
写完文件后别忘了加 BOM。我这里是写完后重新处理一次字符串,更简单的方式是写入前直接在前面拼 \uFEFF。加完 BOM 的文件,Excel 和 WPS 都能正确识别中文。
Future<String> exportToCsv(List<HealthRecord> records) async { final buf = StringBuffer(); buf.write('\uFEFF'); buf.writeln('记录时间,体重,体脂率,收缩压,舒张压,心率,睡眠时长,症状备注'); for (final r in records) { final line = <String>[ _formatReadable(r.recordTime), r.weight.toString(), r.bodyFatRate.toString(), r.systolic.toString(), r.diastolic.toString(), r.heartRate.toString(), r.sleepHours.toString(), CsvWriter.escape(r.note ?? ''), ].join(','); buf.writeln(line); } final dir = await getTemporaryDirectory(); final timestamp = _formatTimestamp(DateTime.now()); final filePath = '${dir.path}/health_records_$timestamp.csv'; await File(filePath).writeAsString(buf.toString(), flush: true); return filePath; }上面的实现里,数字字段直接调 toString,不要用三目运算做"空值显示为 0"之类的手脚。体重没记录就是没记录,导出成空字符串比导出成 0 要诚实,医生看到 0 和看到空值的解读完全不一样。
3.3 性能和线程:大数据量导出不能卡 UI
健康记录 App 的数据量初期不大,但用户的记录如果积累了一两年,每天几条,总量也可能上万条。如果直接在主 Isolate 里读数据库、拼字符串、写文件,App 界面会卡顿甚至触发 ANR。
sqflite 的查询本身在异步线程执行,这部分问题不大。真正耗时的是大量记录的 JSON 序列化和 CSV 字符串拼接。实测下来,一万条记录进 JSON 序列化大概耗时 300 到 600 毫秒,CSV 拼接更慢一点,主要是字符串拼接和转义判断。这个时长在真机上用户是能感知到的,尤其低端设备更明显。
处理办法是在 compute 函数里做序列化。compute 是 Flutter 提供的静态方法,可以把一个函数放到后台 Isolate 执行。需要确保传进去的对象和返回的对象都是可发送的类型,所以不要让后台 Isolate 直接操作 sqflite 数据库对象,而是把查询结果先拿回来,再传给 compute 去拼字符串。
final csvString = await compute(generateCsvContent, records); await File(filePath).writeAsString(csvString, flush: true);这里有一个体验上的小技巧:生成 CSV 内容之前,先弹一个轻量的加载提示,等 compute 完成后再关闭。因为 compute 期间页面无法响应,如果操作需要一两秒,没有提示用户会觉得点了没反应。我这里是用了悬浮的 CircularProgressIndicator,不过如果你不想做这个,至少要把导出按钮做成 loading 状态,防止用户重复点击生成多个文件。
数据库查询和数据序列化分离还有一个额外的好处:后面如果要在导出流程里加入加密、压缩、上传云端,都可以在这些环节之间插入独立的处理步骤,不会把整个流程越搞越长。
4. 从导出到分享:分享面板与平台通道
4.1 分享文件时遇到的第一道坎
文件生成之后,下一个核心动作是让用户能把文件送出去。我这里所说的"送出去"包括:分享到微信、QQ、邮件,以及保存到系统文件管理器。我第一次实现时直接用 share_plus 插件的 share 方法传入文件路径,在 Android 上很顺利,但在 OpenHarmony 上踩了个大坑:分享面板能弹出来,但接收方拿到的文件是空的,或者是 0 字节文件。
问题出在 share_plus 对 OpenHarmony 的适配还不完善,它在调用系统分享组件时没有正确传递 URI 的文件读取权限,系统拿到 URI 后无法读取沙箱内的文件。后来我换了老版本的 share_plus,走了一次 Media Library 的授权,才把文件真正传给接收方。
如果不想在插件适配问题上纠缠,还可以绕开 share_plus,直接用 MethodChannel 在 Native 侧拉起 OpenHarmony 的分享面板。这种方式最稳妥,因为 share_plus 最终也是通过 Native 侧的系统服务来实现分享,你自己写通道反而更可控,能针对 OpenHarmony 的 API 做精确适配。
建议你在导出功能进入正式版之前,至少验证一下:文件生成后手动用文件管理器去看一遍,确认字节数是否正常;再分享给微信和邮箱,确认接收方打开文件的内容完整。如果这两步都过了,说明分享链路基本可靠。
4.2 用 MethodChannel 接入系统分享面板
我用 MethodChannel 做了一个自己的分享通道,Dart 侧定义方法名 exportShare,Native 侧接收文件路径和 MIME 类型,然后调用系统组件拉起分享面板。Dart 侧代码如下:
static const _channel = MethodChannel('com.example.health/export'); Future<void> shareFile(String filePath, String mimeType) async { try { await _channel.invokeMethod('exportShare', { 'filePath': filePath, 'mimeType': mimeType, }); } on PlatformException catch (e) { debugPrint('share file failed: ${e.message}'); } }Native 侧如果你是用 DevEco Studio 开发 OpenHarmony 应用,需要在对应的 Ability 里处理 MethodChannel 分发。OpenHarmony 的 Flutter 插件开发方式跟 Android 不完全一样,它使用的是 Stage 模型,需要在 EntryAbility 的 onWindowStageCreate 里注册 FlutterEngine 和 MethodChannel。
Native 侧拉起分享组件之前,注意检查文件是否存在。如果文件不存在,系统分享面板会弹出一个解析失败的错误,用户体验极差。这一步属于防御性编程,但很多人在写 Native 代码时容易忽略。
此外,MIME 类型的设置也值得认真填。CSV 对应的标准 MIME 是 text/csv,JSON 是 application/json。如果 Type 设置错了,分享面板会把文件当作未知类型,导致微信里显示"文件格式不支持预览"。实测发现微信对 CSV 的预览支持不算好,但邮件客户端通常没问题。
4.3 文件有中文名字和特殊空格要注意
给文件起名时不要用中文名,尽管文件系统能支持,但分享出去经过各种应用中转、编码转换后,非常容易变成乱码。我统一使用英文前缀加时间戳,例如 HealthRecords_20250306_143300.csv。这样在微信、邮件、网盘之间流转都稳定。
文件名中的时间分隔符也不要带冒号。Windows 文件路径中冒号是保留字符,虽然文件生成在 OpenHarmony 上可能没问题,但接收方如果是 Windows 用户,文件名里有冒号会导致文件保存失败。用横线和下划线代替,是最稳妥的做法。
空格的坑我也踩过一次。生成的临时目录路径中一旦包含空格,某些分享组件的 URI 解析会把路径切断,导致接收方拿到损坏的文件。这个问题比较隐蔽,因为多数开发机的用户名没有空格,直到我测试同事的电脑目录里带了空格才暴露出来。处理方案是对文件路径做一次 Uri encode 或者严格拼接 URI,不要直接字符串拼一个 file:// 路径。
5. 常见问题与排查技巧实录
5.1 导出成功却找不到文件
这是我在 OpenHarmony 上遇到的最多问题,没有之一。用户点击导出之后,App 弹窗提示"导出成功",但用系统文件管理器翻遍了 Download 和 Documents 目录,就是看不到文件。原因就是我前面说的:path_provider 返回的应用私有目录,对外部文件管理器不可见。
排查方法是先拿到实际写入路径,用 Dart 日志打出来,然后去真机上用文件管理器尝试导航到这个路径。如果文件管理器进不去那个目录,基本可以确认是私有沙箱路径。解决办法有两个方向:一是引导用户通过分享面板把文件分享出去,二是把文件拷贝到公共媒体库目录并通知系统扫描。
我最终两个方案都用上了。导出列表页保留了全部导出记录,点击某条记录可以进入"分享"和"保存到公共目录"两个操作。这样用户既能在 App 内找到历史导出文件,又能把常用文件放到系统文件管理器里,一举两得。
5.2 权限申请了但仍然提示拒绝访问
OpenHarmony 的权限模型里有 normal 级别和 system_basic 级别权限。普通的文件读取权限申请了就能用,但涉及公共目录写入或媒体库写入的权限,有时需要用户手动在设置里打开"允许访问所有文件"。
在 Flutter 侧,你申请权限时用的插件可能拿不到真实的授权状态。我遇到过调用 permission_handler 的申请成功后,实际写公共目录仍然被拒绝的情况,最终原因是插件返回的状态并不对应到 OpenHarmony 的权限授予流程。这种情况下,建议在 Native 侧写一小段权限检查代码,通过 MethodChannel 返回真实状态,别过于依赖跨平台插件的判断。
还要注意,OpenHarmony 的权限弹窗和 Android 有差异,部分系统版本在连续拒绝两次后会把权限申请入口隐藏,用户需要去设置页手动开启。如果遇到"权限申请了,弹窗却一直不出现"的情况,大概率是权限请求次数过于频繁触发了系统限流。此时引导用户去设置里手动开启,比反复弹窗申请更有效。
5.3 大数据量导出时界面卡死
前面提到了用 compute 把序列化放到后台,但如果你一开始没做,十万条记录的 CSV 拼接足以让 Flutter UI 卡住几秒钟,甚至触发系统无响应弹窗。这个坑在测试阶段不容易暴露,因为测试数据量往往只有几十条,要到用户真实数据量级别才会爆发。
另一个隐蔽问题是内存暴涨。CSV 的 StringBuffer 如果不断追加行,最终整个文件内容会驻留内存。如果记录包含很长很长的症状备注,内存占用会明显上升。更稳妥的方式是分块写入文件:每拼 1000 行就调一次 File.openWrite 的 add 方法,把数据刷入文件,然后清空缓冲。这样即使记录有几十万条,内存占用也能控制在稳定范围。
如果你后面的版本要支持导出 Excel 或 PDF,同样要注意分块思想。千万不要觉得"用户数据量不大,一次性生成没问题",健康记录这种长期数据,一年两年累积下来,增长幅度远超预期。
5.4 分享面板空白或无法预览文件
分享面板能弹出但内容空白,多半是文件 URI 的权限没有传给系统。这个问题在 Android 和 OpenHarmony 上同样常见,区别在于 OpenHarmony 的某些系统版本要求你先申请 URI 临时授权,然后才能把 URI 传给分享组件。在 Native 侧,记得在拉起分享能力之前,调用 grantUriPermission 或者对应的授权接口。
无法预览 CSV 则主要是 MIME 类型和文件后缀不匹配。比如你把 MIME 设成 application/octet-stream,系统不知道用什么应用打开,预览自然失败。设置为 text/csv 后,文本类应用就能处理。如果还不行,可以试试不同的 MIME 别名,偶尔 text/plain 比 text/csv 的兼容性更好,因为有些应用只登记了 text/plain 的监听。
还有一个细节:分享后清理临时文件。临时目录里的文件如果不定期清理,用户反复导出会产生大量废弃文件,占用空间。我的做法是:导出成功后生成一个新的文件,同时在应用启动时扫描临时目录,删除超过 7 天的导出缓存。但是注意,如果用户刚刚分享了一个文件,而接收方还没有下载完成,你不能在分享回调前把临时文件删掉。所以清理操作一定要在分享回调回来之后再触发。
6. 验证与最终总结
6.1 在 OpenHarmony 上做回归验证的清单
导出功能涉及文件系统和跨应用能力,不能只在模拟器上跑一遍就算完。我整理了一份回归测试清单,每次改完相关代码都照着过一遍:第一,导出 JSON 后检查文件大小是否大于 0,用文本编辑器打开确认 JSON 结构完整;第二,导出 CSV 后确认表头和记录行数是否与数据库一致,重点检查备注字段里含逗号和换行符的记录;第三,中文是否乱码,用 Windows 环境的 Excel 或 WPS 打开验证;第四,分享到微信和邮件,确认接收方能正常打开附件;第五,连续导出多份文件,确认文件名不冲突;第六,进入文件管理器确认公共目录下的文件可见,且能通过其他应用打开。
测试设备上我建议至少找一台 OpenHarmony 手机和一台平板。平板的屏幕显示逻辑和分享面板样式与手机不同,可能暴露布局上的适配问题。此外不要跳过弱网环境的测试,分享到大文件时如果网络差,可能导致接收方下载超时,这个和 App 本身无关,但用户会认为是 App 问题。
6.2 导出功能还能怎么扩展
导出这块功能做完了之后,我的下一步规划是支持数据导回,也就是把导出的 JSON 文件重新导入 App,实现完整的备份恢复闭环。实现方法是解析 JSON 文件后逐条写入数据库,期间要处理主键冲突和数据校验,比如导入的记录时间不能和现有数据重复。
如果你要导出 Excel,可以考虑用 syncfusion_flutter_xlsio 或者自己做 XML 表格结构。对健康记录来说,Excel 比 CSV 的优势是可以带单元格样式,比如异常血压高亮显示、趋势图嵌入,但这部分工作量会明显增加,建议按需推进。
做数据导出这个功能,我心里最大的感受是:跨平台的边界比想象中多得多,真正的坑往往不在 Flutter 层,而在你对目标系统的理解深度。OpenHarmony 虽然 API 风格上和 Android 有相似之处,但沙箱机制、媒体库授权、分享组件的 URI 处理都有自己的一套逻辑,光靠"Android 能跑就照搬"的思路,到头来一定会在真机测试时被现实教育。建议你在动手写导出之前,先把 path_provider、权限插件、share_plus 这几个基础依赖在当前 Flutter for OpenHarmony 版本上的兼容性查一遍,再决定哪些用现成插件、哪些自己写 MethodChannel,能省下大量排查时间。