写这篇的时候,我正坐在办公桌前对着 RK3568 开发板发呆。起因很简单:社团要做一个内部管理的 App,既要管成员信息,又要记收支账目,还要在 OpenHarmony 设备上跑起来。团队里有人提议用 ArkTS 从头写,有人提议用 Flutter 快速出活,后来我们选了第二条路,然后一头扎进了"Flutter for OpenHarmony"这个还不太成熟但确实能跑通的生态里。这篇文章就把我做收支记录模块的过程完整复盘一遍,包括选型理由、环境搭建、数据模型设计、UI 实现、持久化方案,以及在真机上调试时踩到的一堆坑。如果你正准备在 OpenHarmony 上做 Flutter 应用,尤其是带财务记账这种业务模块的,这篇应该能帮你少走不少弯路。
1. 为什么在 OpenHarmony 上选了 Flutter:三条技术路线的实际对比
先说结论:OpenHarmony 上做应用开发,目前主流路子有三条,各有各的适用场景,没有绝对好坏,但结合"社团管理 App 收支记录"这个具体需求,Flutter 的性价比是最高的。
1.1 三条路线:ArkTS 原生、Flutter 跨端、React Native 跨端
第一条是纯 ArkTS 开发。这是 OpenHarmony 的"亲儿子"路线,IDE 支持最完善,文档最全,API 调用最直接。但问题也很现实:ArkTS 的生态成熟度跟 Flutter 比还是差了一截,尤其是第三方 UI 组件库、图表库、日期选择器这些东西,能找到的现成方案比较少,很多时候得自己造轮子。社团管理 App 里有个收支统计图表,如果用 ArkTS 画折线图和柱状图,工作量不小。
第二条是 React Native for OpenHarmony。RN 的优势是前端圈子大、热更新机制成熟,但 OpenHarmony 上的 RN 适配进度一直不温不火,很多原生模块没有对应实现,遇到问题很难找到解决方案。我身边有朋友试过,最后被各种桥接错误劝退了。
第三条就是 Flutter for OpenHarmony。Flutter 的渲染引擎是自己实现的,不依赖系统原生控件,这意味着它的跨端一致性和可移植性天然比 RN 好。OpenHarmony SIG 社区有一个专门的 flutter_flutter 仓库在做适配,目前已经能跑起大体量应用了。对我们这种需求明确的业务型 App 来说,Flutter 的 UI 开发效率确实高,一套代码将来还能复用到 Android 和 iOS,社团里如果有人在用 iOS 手机,后续做移动端版本也方便。
1.2 收支记录模块对技术选型的三个关键要求
我做选型的时候,给收支记录模块列了三个硬性要求,这三个要求直接决定了 Flutter 是最优解。
第一是表单交互要流畅。收支记录的核心操作是录入数据,涉及到金额键盘、分类选择、日期选择、备注输入这几个控件。Flutter 的 TextField、showDatePicker、BottomSheet 这些内置组件在 OpenHarmony 上适配得不错,做出来的交互手感和原生 App 很接近,不需要自己实现复杂的手势逻辑。
第二是数据统计要直观。社团的收支记录不是记完就完事了,期末要汇总、要按活动分类看开销、要对比收入支出占比。Flutter 生态里有 fl_chart 这样的图表库,虽然 OpenHarmony 适配版可能需要自己处理一些细节,但比从零写一个图表库省太多事了。
第三是团队上手成本要低。社团的开发者都是学生,水平参差不齐。Flutter 的 Dart 语言语法简单,UI 是组合式写法,新人培训成本比 ArkTS 和 RN 都低。我让两个之前只写过 Vue 的同学直接上手写页面,一天就能出活了。
提示:如果你们的应用对系统深层次能力(比如分布式软总线、蓝牙、USB 等)依赖很重,那别犹豫,直接走 ArkTS 原生路线。Flutter for OpenHarmony 目前的定位还是偏"普通应用场景",系统级 API 的覆盖不如原生全面。我们的 App 只需要本地记账和数据库,不涉及分布式能力,所以 Flutter 完全够用。
2. 搭环境这一步,比你想的更费劲:DevEco Studio、hdc 和 Flutter SDK 的三角关系
OpenHarmony 的 Flutter 开发环境搭建跟 Android 不一样,不是装个 Flutter SDK 就完事了。你需要同时搞定 OpenHarmony 的 IDE、命令行工具和 Flutter 的 OpenHarmony 分支,这三者之间的版本匹配关系一旦搞错,后面全是问题。
2.1 工具链清单和版本匹配表
我折腾了几轮之后,最终稳定的版本组合是这样的:
| 工具 | 版本 | 备注 |
|---|---|---|
| DevEco Studio | 4.0 Release | 带 OpenHarmony SDK,API 10 |
| OpenHarmony SDK | API 10 | 在 DevEco 里通过 SDK Manager 安装 |
| hdc | 随 DevEco 附带 | 路径在 SDK 目录下的 toolchains 里 |
| Flutter SDK | flutter_flutter 仓库的 ohos 分支(基于 Flutter 3.7.x) | 不要用官网原版 Flutter |
| Dart SDK | 随 Flutter SDK 附带 | 版本 2.19.x |
这里最关键的认知是:Flutter for OpenHarmony 不是一个独立的框架,而是 Flutter 官方 SDK 的 fork。OpenHarmony SIG 在 GitHub 上维护了 flutter_flutter 和 flutter_engine 两个仓库,你需要拉取这些仓库的特定分支,而不是用 flutter.dev 官网下载的 SDK。
2.2 拉取和配置 Flutter SDK 的完整命令
# 拉取 flutter_flutter 仓库的 ohos 分支 git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git # 拉取 flutter_engine 仓库(用于编译引擎,如果只是开发可以不主动拉) git clone -b ohos https://gitee.com/openharmony-sig/flutter_engine.git # 把 flutter/bin 加入 PATH export PATH="$PATH:$(pwd)/flutter_flutter/bin" # 运行 flutter doctor,检查 OpenHarmony 相关配置 flutter doctor --verboseflutter doctor 的输出会指导你安装一些 Android 工具链,但注意:OpenHarmony 开发并不需要完整的 Android SDK,devicectl 那部分可以忽略,只要看到 flutter、Dart、DevEco Studio 这部分没问题就行。
然后你需要在项目的 pubspec.yaml 里配置一个额外的依赖源,因为部分 OpenHarmony 适配的包不在 pub.dev 上:
# pubspec.yaml environment: sdk: ">=2.19.0 <4.0.0" dependencies: flutter: sdk: flutter provider: ^6.1.1 path_provider: ^2.0.15 hive: ^2.2.3 hive_flutter: ^1.1.0 # 如果想用 OpenHarmony 移植版的社区包,可以加下面的源 # 但实际上核心包官方 pub.dev 已经有了,不需要额外配源2.3 创建 Flutter 工程并适配 OpenHarmony 目录
创建一个普通 Flutter 工程之后,你需要用 DevEco Studio 打开工程,然后它会自动识别出 Flutter 模块,补充生成 OpenHarmony 的工程外壳。这一步我没法给出一套"必定成功"的 GUI 操作指南,因为 DevEco Studio 每个小版本菜单有一点点差异,但整体思路是:
- 在 DevEco Studio 里选择"导入 Flutter 工程"。
- 指定到你的 flutter 项目根目录。
- DevEco 会提示你配置 SDK 和 hdc 路径。
- 工程生成后,你会看到一个
ohos目录,这就是 OpenHarmony 侧的壳工程。
注意:
ohos目录是自动生成的,不要手动往里面添加 Flutter 代码。你所有的 Dart 代码依然写在lib目录里,然后通过 flutter build hap 命令构建出 OpenHarmony 的应用包(HAP 文件)。
2.4 hdc 的坑:先用hdc version验证连接
hdc 相当于 Android 的 adb,是连接开发板和模拟器的核心工具。配置 DevEco Studio 的时候,它内部会自己找 hdc,但如果你想在命令行里手动安装应用、看日志,就得把 hdc 加到系统 PATH。
我看热搜词里有hdc 查看 openharmony 系统版本 param get,这个确实是常用命令。配好 hdc 之后,用下面这条命令验证连接:
hdc version # 输出: HDC version 1.1.0 这种 # 查看连接的设备 hdc list targets # 查看 OpenHarmony 系统版本(对应热搜里的 param get 用法) hdc shell param get const.ohos.fullname如果你连了设备但 hdc list targets 是空的,先查两件事:开发板有没有开启 USB 调试模式,以及 DevEco Studio 里的 hdc 服务有没有占用端口。后一个坑我后面细讲。
3. 收支记录的数据模型:先想清楚"记账"这件事的逻辑再写代码
很多新手写收支记录,上来就建一个表:id、金额、备注。然后写着写着发现要加分类、加支付方式、加活动关联,表结构改来改去,数据又要迁移。社团账目虽然简单,但一样要按业务逻辑去设计模型。我在做这一块的时候反复推演了几次,最终定下来这样一套结构。
3.1 收支记录的核心字段设计
一条收支记录(LedgerRecord),至少要包含这些信息:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 使用 UUID,不用自增 id,方便将来多端同步 |
| type | int | 0 表示支出,1 表示收入 |
| amount | double | 金额,单位是元,保留两位小数 |
| category | String | 分类编码,如 "activity"、"equipment"、"grant" |
| title | String | 摘要标题,比如"社团迎新晚会物料费" |
| note | String | 备注,可空 |
| occurredAt | DateTime | 实际发生时间 |
| createdAt | DateTime | 记录创建时间 |
| operatorId | String | 经手人,关联到成员表 |
设计的时候有两个关键决策值得展开说一下。
第一个决策是金额用 double 还是 int(分)。财务系统通常推荐用整数分,避免浮点误差。但社团 App 金额通常不大,而且 Dart 的 double 在加减运算时误差可控,所以我用了 double。如果你担心精度,可以在存储层转成 int 分存储,展示层除以 100。我在项目里给了一个工具方法:
int toFen(double yuan) => (yuan * 100).round(); double toYuan(int fen) => fen / 100;第二个决策是分类是固定枚举还是动态维护。社团收支场景相对固定,我预设了几个分类,但允许用户在设置里自定义。分类用字符串编码而不是 int 索引,这样新增分类不需要改数据库结构。
class ExpenseCategory { final String code; final String name; final bool isIncome; const ExpenseCategory(this.code, this.name, {this.isIncome = false}); } const List<ExpenseCategory> defaultCategories = [ ExpenseCategory('activity', '活动经费'), ExpenseCategory('equipment', '器材采购'), ExpenseCategory('food', '餐饮团建'), ExpenseCategory('transport', '交通出行'), ExpenseCategory('other', '其他支出'), ExpenseCategory('grant', '社团拨款', isIncome: true), ExpenseCategory('donation', '校友捐赠', isIncome: true), ];3.2 状态管理选型:Provider 就是当前阶段的最优解
OpenHarmony 上的 Flutter 状态管理,我试过 Bloc 和 Riverpod,最后选了 Provider。理由很朴素:Provider 的依赖注入模型在 OpenHarmony 的适配层上最稳定,学习曲线也平滑。Bloc 对事件的抽象做得很好,但在 OpenHarmony 上偶尔会有 Stream 相关的兼容问题;Riverpod 写起来很爽,但 flutter_hooks 这类辅助包在 OpenHarmony 上的适配还不完整。
Provider 的核心就三件事:ChangeNotifier 负责管理状态,ChangeNotifierProvider 负责提供状态,Consumer/context.watch 负责消费状态并触发重建。下面是我定义的 LedgerController:
class LedgerController extends ChangeNotifier { List<LedgerRecord> _records = []; bool _loading = false; List<LedgerRecord> get records => List.unmodifiable(_records); bool get loading => _loading; Future<void> load() async { _loading = true; notifyListeners(); _records = await LedgerRepository.instance.getAll(); _loading = false; notifyListeners(); } Future<void> addRecord(LedgerRecord record) async { await LedgerRepository.instance.insert(record); _records.insert(0, record); notifyListeners(); } Future<void> deleteRecord(String id) async { await LedgerRepository.instance.delete(id); _records.removeWhere((r) => r.id == id); notifyListeners(); } double get monthIncome { final now = DateTime.now(); return _records .where((r) => r.type == 1 && r.occurredAt.year == now.year && r.occurredAt.month == now.month) .fold(0, (sum, r) => sum + r.amount); } double get monthExpense { final now = DateTime.now(); return _records .where((r) => r.type == 0 && r.occurredAt.year == now.year && r.occurredAt.month == now.month) .fold(0, (sum, r) => sum + r.amount); } }这里有一个小技巧:notifyListeners 不要在极端频繁的操作里反复调用。我在load()里是先设 loading 再一次性 notify,后面重新赋值 records 后再通知一次,这样 UI 不会因为多次 notify 造成不必要的重建。
3.3 收支录入表单的交互细节
录表单是收支记录最容易做烂的地方。最简单的做法是一排 TextField 加一个保存按钮,但这样做出来的体验很糟糕。我在录入页做了几个针对性的处理。
金额输入框:用 TextInputType.numberWithOptions(decimal: true),并且在输入变化的时候做格式过滤,只允许数字和一个小数点:
TextFormField( keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))], decoration: const InputDecoration(labelText: '金额(元)'), )类型切换:用 SegmentedButton(Flutter 3.7 自带)而不是两个 Tab。因为收入支出切换时,下面的分类列表要跟着变化,SegmentedButton 的 onSelectionChanged 回调可以同步更新分类区域。
日期选择:默认选中"今天",点击后弹出 showDatePicker。这里要注意 OpenHarmony 上 showDatePicker 的默认语言可能是英文,需要在 MaterialApp 里配置本地化委托:
MaterialApp( localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [Locale('zh', 'CN')], locale: const Locale('zh', 'CN'), home: LedgerPage(), )保存前校验:金额必须大于 0,标题不能为空。用 Form + GlobalKey 做统一校验,保存按钮的 onPressed 里先 validate 再提交。
4. 收支列表与月度统计:怎样把账目页面做得"一眼看清"
收支记录的价值在于"回顾",所以列表页和统计摘要页是整个模块的视觉核心。我的目标是让用户打开 App 后,不用任何操作,一眼就能看到:这个月收入多少、花了多少、结余多少、花在哪些地方最多。
4.1 首页 Dashboard:月度汇总卡片
页面顶部放一个"本月概览"卡片,用三个大数字显示收入、支出、结余。这个卡片我用了一个渐变背景的 Container 包着 Column:
Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF4A6CF7), Color(0xFF3B5BDB)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ _SummaryItem(label: '收入', value: controller.monthIncome), _SummaryItem(label: '支出', value: controller.monthExpense), _SummaryItem(label: '结余', value: controller.monthIncome - controller.monthExpense), ], ), )三个数字用等宽字体显示,避免数字宽度变化引起视觉跳动。Dart 里可以用FontFeature.tabularFigures()来实现等宽数字效果,OpenHarmony 的 Flutter 适配支持这个特性。
4.2 分类占比:基于 CircularProgressIndicator 自己画
我原本想引入 fl_chart,但在 OpenHarmony 上试了一下,发现饼状图的交互(点击展示详情)有些兼容问题,后来换了一种更稳妥的画法:用 Flutter 自带的 CircularProgressIndicator 的 value 属性实现环形占比图,再加一个图例列表。
环形占比图本质是多个弧线拼在一起,每个弧线代表一个分类的占比。Flutter 里可以用TweenAnimationBuilder做动画,让它从 0 增长到目标值:
TweenAnimationBuilder<double>( tween: Tween(begin: 0, end: proportion), duration: const Duration(milliseconds: 600), builder: (context, value, _) { return CircularProgressIndicator( value: value, strokeWidth: 18, backgroundColor: Colors.grey.shade200, color: categoryColor, ); }, )但多个弧线拼接时,需要控制value的起止范围,每个分类的value是从start + value到start + value + share。这个用 CircularProgressIndicator 做起来比较别扭,我最后改成用CustomPaint画圆弧,反而更干净。这里贴一个简化版:
class CategoryRingPainter extends CustomPainter { final List<CategoryShare> shares; CategoryRingPainter(this.shares); @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; double startAngle = -Math.pi / 2; for (final share in shares) { final sweepAngle = share.proportion * 2 * Math.pi; final paint = Paint() ..color = share.color ..style = PaintingStyle.stroke ..strokeWidth = 18; canvas.drawArc(rect.deflate(9), startAngle, sweepAngle, false, paint); startAngle += sweepAngle; } } @override bool shouldRepaint(covariant CategoryRingPainter oldDelegate) => true; }设计比例映射关系时,注意圆弧的起始角是 3 点钟方向,通常我们希望 12 点是起点,所以把起始角偏移-Math.pi / 2。
4.3 列表页:按日期分组和左滑删除
收支列表按时间倒序排列,并按日期分组,每组显示"今天""昨天"或具体日期。这里有一个我在 Android 上没有遇到过但在 OpenHarmony 上容易出问题的地方:中文日期和星期的格式化需要精确指定 locale。我在 3.3 里配置了 intl 包,然后调用:
final formatter = DateFormat('yyyy年M月d日 EEEE', 'zh_CN');至于左滑删除,OpenHarmony 上的 Flutter 对 Dismissible 组件适配得还行,可以直接用。但有一个坑:如果列表项是一个 Stack 嵌套多个子组件,Dismissible 的 background 在滑动时可能出现绘制闪烁。解决办法是用ClipRRect包住 Dismissible,并确保每个列表项的 key 用的是记录 id。
Dismissible( key: ValueKey(record.id), direction: DismissDirection.endToStart, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: const Icon(Icons.delete, color: Colors.white), ), onDismissed: (_) => controller.deleteRecord(record.id), child: _RecordTile(record: record), )删除的时候要弹出确认框吗?我的建议是:不要弹,但给一个 SnackBar 撤销按钮。社团场景下既要效率又要容错,左滑删除直接执行,同时显示"已删除,点此撤销",这个交互比弹窗问"你确定吗"自然得多,也更符合 Flutter 的 Material 设计规范。
5. 持久化方案:Hive 在 OpenHarmony 上的适配与坑
收支记录是必须持久化的数据,不可能一直在内存里。我在选持久化方案的时候对比了三种:SharedPreferences、SQLite(sqflite)、Hive。
5.1 为什么是 Hive
SharedPreferences 适合存简单的键值对,不适合存结构化列表。sqflite 是 Flutter 最常用的 SQLite 库,但在 OpenHarmony 上需要 native 层支持,官方适配还不完善,主要问题在于编译时要找 SQLite 的动态库,OpenHarmony 的 musl 库和 Android 的 bionic 库不太一样,经常链接失败。Hive 是纯 Dart 实现的键值数据库,没有原生依赖,天然适合 OpenHarmony 这种新兴平台。
Hive 的缺点是查询能力弱,只能按 key 读取。但我们的场景是"存一个列表 + 按时间倒序展示 + 按月汇总",完全可以在内存里做,数据量(一学期几百条记录)也足够小,所以 Hive 很合适。
5.2 Hive 的初始化和读写
这里直接上代码。首先在 main 里初始化:
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); final dir = await getApplicationDocumentsDirectory(); Hive.init(dir.path); Hive.registerAdapter(LedgerRecordAdapter()); await Hive.openBox<LedgerRecord>('ledger'); runApp(const MyApp()); }注意:getApplicationDocumentsDirectory 是 path_provider 包提供的。path_provider 在 OpenHarmony 上有一个适配版本,需要确保 pubspec 里用的是支持 OpenHarmony 的版本。实测下来 path_provider 2.0.x 在 OpenHarmony API 9+ 上可以正常工作。
定义 Hive 的 TypeAdapter,序列化和反序列化自定义对象:
class LedgerRecordAdapter extends TypeAdapter<LedgerRecord> { @override final int typeId = 0; @override LedgerRecord read(BinaryReader reader) { return LedgerRecord( id: reader.readString(), type: reader.readInt(), amount: reader.readDouble(), category: reader.readString(), title: reader.readString(), note: reader.readString(), occurredAt: DateTime.fromMillisecondsSinceEpoch(reader.readInt()), createdAt: DateTime.fromMillisecondsSinceEpoch(reader.readInt()), operatorId: reader.readString(), ); } @override void write(BinaryWriter writer, LedgerRecord obj) { writer.writeString(obj.id); writer.writeInt(obj.type); writer.writeDouble(obj.amount); writer.writeString(obj.category); writer.writeString(obj.title); writer.writeString(obj.note); writer.writeInt(obj.occurredAt.millisecondsSinceEpoch); writer.writeInt(obj.createdAt.millisecondsSinceEpoch); writer.writeString(obj.operatorId); } }TypeAdapter 的序列化顺序必须和 write/read 严格一致,否则会读出脏数据。这是一个非常容易踩的坑,尤其是你中途往模型里加了字段,如果没有更新 Adapter,旧数据读出来会对不上。
5.3 模拟异常关闭导致的数据破损
Hive 本身有 recovery 机制,但如果 App 在写入过程中被强杀或者开发板突然断电(开发板经常这样),Hive 文件可能损坏。我在测试的时候遇到过两次打开 Box 直接抛异常的情况。解决办法是在初始化 Hive 时做一层 try-catch 保护:
Future<void> safeInitHive() async { final dir = await getApplicationDocumentsDirectory(); Hive.init(dir.path); try { await Hive.openBox<LedgerRecord>('ledger'); } catch (e) { // 如果文件损坏,先删掉再重建,避免启动崩溃 await Hive.deleteBoxFromDisk('ledger'); await Hive.openBox<LedgerRecord>('ledger'); } }这在开发调试阶段很有用,正式上线时还要考虑导出备份逻辑,但至少不会因为一次异常关机就让整个 App 打不开。
6. 真机联调与打包:RK3568 开发板上的实测记录
写完代码只是第一步,真机跑起来才是真正的考验。我这里用的是 RK3568 开发板,OpenHarmony 3.2 Release 系统。下面这条链路是我踩过无数坑之后总结出来的最短路径。
6.1 hdc 连接与常见故障排查
先讲连接。开发板通电,USB 线连到电脑,USB 调试模式打开。命令行执行:
# 先杀掉可能残留的 hdc server hdc kill # 启动 hdc server hdc start # 查看设备 hdc list targets如果 hdc list targets 能看到设备序列号,说明连接成功。如果什么都看不到,按照这个顺序排查:
- 检查开发板设置里的"开发者选项",确认 USB 调试已开启。OpenHarmony 有些镜像默认关闭,需要在设置里手动打开。
- 换一根支持数据传输的 USB 线。很多 Type-C 线只能充电,不能传数据,这个坑仅次于 Wi-Fi 信号差。
- 检查 hdc 服务端口占用。DevEco Studio 自己会启动一个 hdc server,占用端口 8710。如果你之前在命令行里启动过 hdc,可能会冲突。最简单的办法是
hdc kill然后再hdc start,或者把 DevEco Studio 先关掉。 - 看系统日志:
hdc hilog可以输出设备日志,连接上了但没日志,那多半是设备系统本身有问题。
6.2 构建 HAP 包并安装到开发板
在项目根目录执行:
flutter build hap --debug这会调用 OpenHarmony 的构建链,生成 HAP 包。输出位置一般在:
build/ohos/phone/outputs/hap/<project_name>_phone_debug.hap然后通过 hdc 安装:
# 安装应用 hdc install ./build/ohos/phone/outputs/hap/my_app_phone_debug.hap如果安装时报 INSTALL_PARSE_FAILED 之类错误,先确认 HAP 是不是用了与设备匹配的 OpenHarmony SDK 版本。API 10 的 HAP 装到 API 9 的板子上大概率会失败。
运行应用:
hdc shell aa start -a MainAbility -b com.example.myappcom.example.myapp是你的 OpenHarmony 应用的 bundleName,在 ohos 目录下的 module.json5 或 build-profile.json5 里配置。
6.3 日志查看:Flutter 日志和 OpenHarmony 日志怎么分开看
开发中最常用的命令是:
# 查看所有 hilog hdc hilog # 过滤 Flutter 的日志(Flutter 会打到 stdout) hdc shell hilog | grep -i flutter不过 hdc hilog 的输出量非常大,建议在 Flutter 代码里加 debugPrint,然后通过hdc hilog | grep flutter过滤。如果你在 IDE 里调试,DevEco Studio 的 Log 面板也可以直接看到 Flutter 的 print 输出,但有时候会卡,不如命令行来得直接。
6.4 性能表现:掉帧和耗电
RK3568 的性能比主流手机弱不少,Flutter 应用在上面跑会有明显的掉帧。我的实践经验是:
- 不要在列表里用图片加载框架(比如 cached_network_image),网络图片用原生
Image.network就够了,OpenHarmony 上 Flutter 的 HttpClient 走的是 Wi-Fi 栈,缓存机制比较少。 - 避免过度使用阴影和模糊效果。BoxShadow、BackdropFilter 这类视觉效果在 RK3568 上会严重拖慢帧率。我在月度卡片上用的渐变背景没问题,但一开始给列表项加了 BoxShadow,滑动明显不流畅,去掉之后好了很多。
- 热重载有概率失败,在 OpenHarmony 上
hot reload没有 Android 那么可靠,改动多了直接r不行就R全量刷新,再不行就重新 build。
7. 踩坑实录:OpenHarmony 上 Flutter 开发最容易翻车的四个地方
最后集中说一下我在整个开发周期里印象最深的几个坑,每一个都至少浪费了我半天时间。
7.1 mybatis-plus 问题没遇到,但 "apply flutter's main gradle plugin imperatively" 这个问题遇到了
热搜词里有一条you are applying flutter's main gradle plugin imperatively using the apply s...,这是 Android 工程里的 Gradle 报错,OpenHarmony 也类似。它的含义是:你用apply plugin:的方式应用了 Flutter 插件,但新版 Gradle 要求用plugins {}声明式语法。
OpenHarmony 的 flutter 构建流程偶尔会产生这种兼容问题,解决办法是找到 ohos 目录下的 build.gradle 或 settings.gradle,把:
apply plugin: 'dev.flutter.flutter-plugin-loader'改成 plugins DSL 写法,或者升级 Flutter SDK 到修复了此问题的版本。
7.2 第三方包的 OpenHarmony 支持清单
这是最疼的。大多数 pub.dev 上的 Flutter 包都依赖原生代码(Android 的 Kotlin/Java、iOS 的 Objective-C/Swift),在 OpenHarmony 上无法直接运行。我列一个我实测过的包支持情况,供参考:
| 包名 | OpenHarmony 支持情况 | 说明 |
|---|---|---|
| provider | 可用 | 纯 Dart 实现,无原生依赖 |
| hive / hive_flutter | 可用 | 纯 Dart 实现 |
| path_provider | 可用(有适配分支) | 需要引入社区 fork 或等待官方适配 |
| intl | 可用 | 纯 Dart |
| fl_chart | 部分可用 | 能画基本图表,个别动画效果适配不佳 |
| dio | 可用 | 纯 Dart 网络库 |
| cached_network_image | 不推荐 | 图片缓存会失效,性能差 |
| flutter_local_notifications | 不可用 | 依赖系统通知服务,OpenHarmony 未适配 |
| shared_preferences | 可用 | OpenHarmony 有官方适配版本 |
| sqflite | 不推荐 | 原生库链接问题多,建议换 Hive |
对于不支持的包,短期内除了等社区适配,只能自己动手改。如果你只是做业务型 App,尽量把依赖控制在纯 Dart 生态里,可以省去大量移植调核的时间。
7.3 中文字体消失问题
OpenHarmony 系统默认是没有 Flutter 自带的那套 Roboto 字体的,中文渲染依赖系统的 HarmonyOS Sans。如果你在 Flutter 里指定了 fontFamily,可能某些中文字符显示为方框。解决办法:要么不指定 fontFamily,让 Flutter 走系统字体栈;要么在 assets 里打包一个中文字体文件,例如 OppoSans 或 HarmonyOS Sans 的 TTF,然后:
ThemeData( fontFamily: 'HarmonyOS_Sans', // 记得在 pubspec.yaml 里声明 assets/fonts/ 目录 )这个坑只会在 OpenHarmony 上出现,在 Android/iOS 上不会,因为那边的系统中文字体栈是完整的。
7.4 devicectl 相关报错和真机部署的相似坑
Flutter 的 device 发现机制在 OpenHarmony 上不是自动的。flutter devices可能看不到已经连接好的 RK3568,这时候不要慌,直接用 hdc install 手动部署,不要依赖flutter run的自动安装功能。我在使用中flutter run -d <device_id>偶尔能识别,但稳定性不如手动 hdc。所以我的工作流是:flutter build hap --debug生成包,hdc install安装,hdc shell aa start启动。虽然多点几步,但每一步都可控,出问题也好定位。
8. 后续还能怎么扩展:图表升级和多端同步
最后说一点我还在规划中的扩展方向,如果你也是在社团或者校内团队做这类 App,可以参考。
当前版本的中文图是基于 CustomPaint 画的环形占比图,够用但不够丰富。后面我打算把 fl_chart 或 community_chart 的 OpenHarmony 适配搞定,加上折线趋势图,展示月度收支变化曲线。这个需求很常见,很多社团都有"看这个学期经费是稳定增长还是突然超支"的诉求。
另外是同步问题。目前数据只存在本地 Hive 里,如果换手机或者开发板坏了,数据就丢了。后续打算用 OpenHarmony 的分布式数据管理服务来实现多端同步,这样可以在开发板和手机之间共享账目。这个功能目前 Flutter 侧没有现成插件,需要写 OpenHarmony 的原生插件做桥接,工作量不小,但意义也很大。
如果你只是做一个小工具,把 Hive 数据通过云服务手动备份,也是更轻量的替代方案。
做这个收支记录模块前后花了大概两周,真正写业务代码的时间其实很少,大部分时间都耗在环境适配和踩坑上。不过这也是前期做 OpenHarmony 应用必须经历的过程,毕竟生态还没有像 Android 那么成熟。但反过来想,正因为不成熟,才有大量可以自建方案、积累经验的空间。等走过这一段,你对 Flutter 渲染原理、OpenHarmony 系统结构理解都会上一个台阶。