flutter_for_openharmony 油耗追踪器 App 实战:从数据模型到油耗趋势图,一套代码跑通
最近趁着换开发板的间隙,把手头一个开源项目“FillUp”的 OpenHarmony 适配完整跑了一遍。这个项目本身是个很经典的油耗追踪器,功能也不复杂:记录每次加油量、单价、里程数,然后画出油耗趋势、费用趋势。但难点在于,它是用 Flutter 写的,而目标是让它在 OpenHarmony 上跑起来,这就不是单纯移植 UI 那么简单了。折腾了两周,踩了不少坑,最后把整个链路串通,包括数据持久化、满箱油耗算法、图表渲染,全都在 OpenHarmony 设备上稳定运行。
这篇文章我想把这套实战过程完全拆开来讲,从环境适配开始,到油耗计算的业务逻辑,再到趋势图的实现细节,最后是适配 OpenHarmony 时常见的问题排查。内容偏实操,有 Flutter 基础的朋友可以直接照着做,不熟悉的也可以先看思路,毕竟油耗追踪器这类工具的模型结构很典型,搞懂了一个,其他记录类 App 都大同小异。
1. 项目整体设计与技术选型思路
1.1 为什么选 Flutter 而不是原生 ArkTS
OpenHarmony 的官方应用开发语言是 ArkTS,但其实也有 Flutter 的适配分支,叫做 flutter_flutter,它的目标就是把 Flutter 引擎跑到 OpenHarmony 的 Native 层上。我选 Flutter 的理由很直接:项目里已经有现成的 Flutter 代码库,Flutter 的跨平台能力能让我把同一套业务逻辑同时部署到 Android、iOS 和 OpenHarmony,不需要为每个平台重写一套。特别是油耗追踪器这种工具类应用,UI 不复杂,逻辑也不重,用 Flutter 开发效率高很多。
但是,选 Flutter 并不是没有代价。OpenHarmony 上 Flutter 的插件生态还不完善,很多 pub 上的插件直接在新环境里可能编译不过,尤其涉及到原生 API 调用的部分。实测下来,纯 Dart 的包(比如状态管理、日期格式化、图表绘制)基本没问题,但依赖原生平台的包(比如某一些文件路径获取、通知推送)就要谨慎了,好在油耗追踪器恰好属于“纯 Dart 应用”的边界内,这让我省了不少事。
1.2 功能边界和数据模型设计
一个合格的油耗追踪器,至少需要解决三个问题:第一,记录每一笔加油的交易数据;第二,算出真实油耗(升/百公里);第三,把油耗变化趋势直观展示出来。围绕这三点,我把数据模型定义得很简单,就一张表,字段如下:
- 加油日期(timestamp)
- 当前里程表读数(km,整数)
- 加油量(升,保留两位小数)
- 加油单价(元/升)
- 总花费(自动计算,但也会保存,防止单价改动影响历史数据)
- 是否加满(布尔值,这个是油耗计算的关键)
- 备注(可选)
为什么一定要有“是否加满”这个字段?因为油耗计算最可靠的场景是:上一次加油加满,这次加油也加满。两次满箱之间消耗的燃油量,等于本次加油量,而两次满箱之间行驶的距离,等于本次读数减去上次读数。这两个数一除再乘以 100,就是升/百公里。如果某次没加满,那油量变化不准确,算出来也没有意义。我见过很多油耗 App 把“半箱油”“跳枪后补油”这种数据也算进去,最后趋势图全是噪声,就是因为模型没设计好。
1.3 技术栈细节与 OpenHarmony 适配范围
我用到的 Flutter 技术栈就三块:状态管理用 Provider(纯 Dart,无原生依赖);本地存储用 sqlite3(OpenHarmony 有适配实现,可以走 libsqlite3 的动态库);图表绘制用 fl_chart(纯粹用 Canvas 画的,不依赖原生控件)。这三个包是核心,其他像 intl 做日期格式化、sqflite 的替代方案,都是纯 Dart 包装。
在 OpenHarmony 上,SQLite 的接入方式跟 Android 不太一样。Android 上有 sqflite 插件,它通过 MethodChannel 调用原生 API;但 OpenHarmony 的 Flutter 分支还没把 sqflite 的对应适配给完善,所以我换成了在 Dart 层直接加载 libsqlite3.so,通过 FFI(外部函数接口)调用 SQLite 的 C 接口。这条路能跑通,但有一些细节要处理,后面我会专门讲。
2. 环境配置和项目脚手架搭建
2.1 OpenHarmony 版 Flutter SDK 的安装与版本选择
这一步是最大的坑,网上很多教程要么版本过时,要么推荐的仓库源已经不维护了。我折腾下来的正确操作是:
先拉取 flutter_flutter 仓库,注意分支名称,OpenHarmony 适配的独立分支一般是 fixed_version 开头,我用的版本对应 Dart 2.x,Flutter 3.x 的一个稳定快照。拉下来之后,把 flutter 可执行文件路径加入 PATH,注意不能和 Android 版的 Flutter SDK 混用,两个 SDK 的 flutter_tools 补丁逻辑不一样,混用会在 build 的时候出莫名奇妙的编译错误。
接下来安装 DevEco Studio,使用 API 9 的版本,这里不需要太新,因为 Flutter OpenHarmony 适配在某些较新 API 版本上的兼容性反而不稳定。我建议使用 API 9 做基准,等适配稳定后再升级。
配置完两个 SDK 之后,要在命令行里执行 flutter doctor 看看 OpenHarmony 环境是否被识别。如果你看到 “OpenHarmony toolchain” 出现了,说明 flutter tool 已经识别到了 OpenHarmony 的 SDK 路径。如果没识别,需要在环境变量里配置 DEVECO_SDK_HOME 指向 DevEco Studio 自带 SDK 目录。
2.2 创建项目并配置 ohos 目录
用 Flutter 命令创建项目后,默认只有 android、ios、web 等目录。要支持 OpenHarmony,你需要用 flutter_flutter 仓库自带的命令来生成 ohos 目录:
flutter create --platforms=ohos .这个命令会在项目根目录生成一个 ohos 目录,里面是一个完整的 OpenHarmony 工程(类似你手动创建的 arkts 工程)。生成后你需要用 DevEco Studio 打开这个 ohos 目录,它才能识别到工程。注意:ohos 目录里有自己的 native 侧代码,但通常我们不会直接去改它,除非要接原生的 API。
还有个容易踩的坑:ohos 目录的包名(bundle name)默认是从 Flutter 工程里的 org 和 project 名称生成的,它的规则和 Android 的包名定义不一样。同时你会发现在 ohos 工程的 module.json5 里,有 abilities 配置,需要把入口 ability 填成 MainAbility,要确保它的类型是 page,不然 Flutter 的入口无法拉起。
2.3 依赖库的版本兼容性处理
由于 flutter_flutter 分支在第三方的 pub 包适配上有滞后,我在处理依赖时养成一个习惯:先看这个包是否依赖 dart:io 之外的 native 库,其次看是否有 OpenHarmony 的兼容实现。对于 fl_chart 这种纯 UI 图表库,我直接选最新版没有问题。对于 intl 包,我用 0.18.0 版本,后面的 0.19 在部分 Flutter 3.7 的分支上有因语言区域处理导致的编译失败。Provider 我用的 6.x 稳定版,没有遇到问题。
sqlite3 包比较关键。它的底层依赖 sqlite3_flutter_libs,这个包会在 Android 上自动把 libsqlite3.so 打包进去。但 OpenHarmony 环境下,这个包没有对应的能力,所以你要手动管理 sqlite3 的动态库。我建议是换个思路:直接用 sqlite3 包提供的 open.overrideForOperatingSystem 函数,指定加载路径,指向 OpenHarmony 系统自带的 libsqlite3.z.so。
具体加载代码如下:
import 'package:sqlite3/sqlite3.dart'; import 'dart:ffi'; import 'dart:io'; void initSqlite() { final libraryPath = 'libsqlite3.z.so'; final open = DynamicLibrary.open(libraryPath); sqlite3.open.overrideFor(OperatingSystem.linux, () => open); }这段代码的 intent 是绕过 sqlite3_flutter_libs 的自动加载机制,直接把系统库加载进来。OpenHarmony 的 musl 库相对标准,sqlite3 的 C API 能被 Dart FFI 正确识别。
注意:路径写死成 libsqlite3.z.so 时,不同 OEM 厂商定制的 OpenHarmony 系统里库名称可能不同,稳妥做法是在应用启动时先检查下系统 lib 目录,或者直接在你的 ohos 目录的 CMakeLists 里把 sqlite3 源码编译进去,这样最保险。我为了省事直接用系统库,实测在标准 OpenHarmony 镜像上是没问题的。
3. 业务逻辑:油耗计算与趋势数据结构
3.1 满箱法计算油耗的原理和公式
油耗计算的核心是两次满箱之间的数据。算法逻辑如下:
- 查询所有加满的加油记录,按日期排序。
- 从第二条满箱记录开始,计算当前记录与前一条满箱记录的差值。
- 行驶里程 = 当前里程表读数 - 前一条里程表读数。
- 燃油消耗 = 当前加油量(因为两次都是满箱,加油量就是实际消耗)。
- 油耗 = 燃油消耗 / 行驶里程 * 100,单位是 升/百公里。
这么算的前提是假设油箱容积固定,而且加油站的跳枪标准基本一致。现实中不同的加油站,油枪灵敏度不同,可能导致满箱线轻微浮动,所以单次计算的油耗会有小误差。但趋势图的意义就在于,把多个数据点连起来看,平均之后能消除随机误差。
我用一个简单例子演示:假如第一次加满时里程表是 1000 公里,加了 50 升,之后正常开;第二次在同一油站加满时里程表是 1600 公里,加油量是 45 升。那么这 600 公里消耗了 45 升油,油耗就是 45 / 600 * 100 = 7.5 升/百公里。
代码实现时,我直接把原始记录按日期取出来,在 Dart 层做这个计算,不复杂的 SQL 关联。这样计算逻辑可以统一复用,而且出现问题方便调试。
3.2 里程间隔计算和异常值过滤
油耗数据最怕脏数据。比如某次输入里程时少打了一个零,或者加油时油箱没满也勾了满箱标志,这些都会产生离谱的油耗值。我在读取数据后加了一个过滤步骤,把单次油耗小于 3 或者大于 20 的记录直接忽略(当然极端情况下有高性能车或混动车可能超范围,但作为普通油耗工具,这个区间合理)。
另外,还有一个边界情况:第一次加油记录没有前一条满箱记录,所以它无法计算油耗。这种情况直接把第一笔记录显示成“初始记录”,不参与油耗计算。UI 上这时图表就是空的,需要给一个空状态提示,告诉用户“再记录两次满箱加油后显示趋势”。
3.3 趋势数据结构设计
为了画趋势图,我把计算后的数据组织成一个轻量模型:
class FuelEffRecord { final DateTime date; final double liters; final double cost; final double odometer; final double fuelEfficiency; final double pricePerLiter; }注意,我没保存每次的总花费,而是通过 加油量 * 单价 动态计算,这样可以随时调整油价而避免修改历史。但在展示历史详情的时候,我保留了一个 snapshot 字段,防止数据被重新计算时波动。
趋势图中需要两条曲线:一条是油耗(升/百公里),另一条是油价(元/升)。这两个数据的量纲不同,所以不能直接画在同一坐标系里。我的方案是:油耗曲线用左 Y 轴,油价曲线用右 Y 轴,fl_chart 里支持 dual_axis,而且两个数据系列的时间戳完全相同,所以 X 轴统一用日期。
另外我还实现了一个低成本的可视化方式:累计加油金额随里程的堆叠展示。这个计算就是每次记录的花费累加,横轴是时间,纵轴是累计金额,用来展示“这台车到底吃掉了多少钱”。虽然它和油耗趋势是同一个数据源,但用不同的聚合视角,对用户的决策帮助是不同的。
4. 数据的持久化与查询实现
4.1 sqlite3 的 FFI 封装细节
我在 Android 上用的是 sqflite 插件,这个在 OpenHarmony 上不可用。改到 sqlite3 FFI 之后,我发现它的异步模型和 sqflite 很不一样。sqlite3 包的同步 API 很直接,它能在 Dart 里同步执行 SQL,这在 UI 线程上会卡帧,而且 OpenHarmony 设备性能偏弱,所以我必须把这些操作放进 isolate 里,或者说用 compute 而不是直接在事件循环里跑数据库。
但 isolate 里使用 sqlite3 需要小心,因为 sqlite3 的数据库对象不是 isolate 之间共享的。常规做法是在主 isolate 打开数据库,把查询语句放到 compute 里执行,但每个 compute 都需要重新打开连接。为了省事,我直接在数据库访问层用了一个全局的线程池抽象,每次请求创建新的连接,用完之后关掉。其实对单机 App 来说,这个开销完全可以接受。
查询代码大概是:
List<RefuelRecord> getAllRecords() { final db = sqlite3.open(databasePath); final result = db.select('SELECT * FROM refuel_records ORDER BY date ASC'); final records = result.map((row) => RefuelRecord.fromRow(row)).toList(); db.close(); return records; }这个操作在 compute 调用时会吞掉一部分创建连接的时间,但胜在简单。
4.2 数据库表结构和迁移
建库语句我放在应用启动的时候执行,用CREATE TABLE IF NOT EXISTS,这样新装用户和升级用户都能稳定接入。
CREATE TABLE IF NOT EXISTS refuel_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER NOT NULL, odometer INTEGER NOT NULL, liters REAL NOT NULL, price_per_liter REAL NOT NULL, is_full_tank INTEGER NOT NULL DEFAULT 1, note TEXT DEFAULT '' );字段类型用 INTEGER 存时间戳毫秒值,这是最省心的做法,比较也好写。REAL 存浮点数,虽然会有精度误差,但油耗计算最终的结果用到小数点后至少一位,float 的误差在小数点后四位以内,完全不影响。对于这种记录类数据,我不建议用 TEXT 存 ISO 格式日期,查起来麻烦,排序也容易出错。
另外,我在数据库的初始化函数里加了 PRAGMA 设置,把 journal_mode 设为 WAL,能显著提高重复读写速度。
4.3 与 OpenHarmony 沙箱路径的配合
OpenHarmony 应用获取数据目录的路径方式是使用 AbilityContext 的能力,但 Flutter 侧往往不知道这个路径。一个实用的做法是:在 MainAbility 里把 context.filesDir 路径通过一个轻量级的原生 MethodChannel 传给 Flutter 侧。
实现方式:
- 在 ohos 侧的 MainAbility 里注册一个自定义 MethodChannel。
- 通过
context.getFilesDir()拿到路径。 - 在 Flutter 侧接收路径后拼接
fuel_tracker.db即可。
如果没有这个路径,你可能会得到权限错误,因为 Flutter 标准库在 OpenHarmony 上没有类似PathProvider的可靠实现。
我踩过一个相关坑:我用Directory.current.path拿到的路径,写到数据库文件里,当时看起来正常,但应用重启后文件可能丢失或不可访问,因为那并不是应用的持久化存储路径。所以路径一定要走原生获取,不要自己猜。
5. 油耗趋势图表的具体实现
5.1 fl_chart 在 OpenHarmony 上的启动避坑
fl_chart 是纯 Dart 绘制,理论上不受平台限制,但我在 OpenHarmony 上确实遇到过一个初始化问题:图表组件在首次 build 时会因为测量宽度为 0 导致内部 assert 失败,这个在 Android 上不常见,但在 OpenHarmony 的某些尺寸计算模式下会触发。
解决方案有三种:
- 给图表组件包裹一个固定高度和宽度的 SizedBox。
- 在图表数据未加载完成前不渲染图表组件,用占位 Container 替代。
- 检查 isNotEmpty 之后再绘制。
我用的是方案 2,因为除非数据加载完成,绘制趋势毫无意义。你也需要确保图表所在的页面构建时,日期和数值列表已经被计算好。这里我推荐把数据计算放在 Provider 的加载函数里,用 FutureBuilder 包裹图表区域。
5.2 双轴曲线的配置细节
fl_chart 画两条曲线的样板代码大致是这样:
LineChart( LineChartData( minX: firstDate.millisecondsSinceEpoch.toDouble(), maxX: lastDate.millisecondsSinceEpoch.toDouble(), minY: 0, maxY: 20, lineBarsData: [ LineChartBarData( spots: fuelEfficiencySpots(), color: Colors.blue, barWidth: 2, ), LineChartBarData( spots: priceSpots(), color: Colors.orange, barWidth: 2, ), ], ), )这里有两个关键参数:右侧 Y 轴要启用rightTitles,可以用SideTitles(showTitles: true, reservedSize: 40)进行预留;如果直接画双序列而忽略右轴,油价曲线的坐标就不是它本身的真实值,视觉上会产生误导。
另外一个必须处理的是 X 轴的日期标签。无论时间跨度是几天还是几个月,你都不希望标签挤成一团。我写了一个工具函数,根据时间跨度动态决定标签间隔:跨度小于 7 天按天显示,小于 30 天按周显示,否则按月显示。这样不管是新车记录还是老车一年数据,图表都保持清爽。
5.3 涂鸦式的手势和提示框交互
在趋势图上加 touch 交互是油耗 App 的重要体验。fl_chart 自带了 touchCallback,可以用它来显示当前触摸点的数据值。我在回调里拿到触摸的 x/y 值后,根据 x 坐标反查出最近的记录,然后在用自定义 Overlay 的方式展示一个浮层。
实现细节有两个要注意的地方:第一,浮层的坐标必须相对于图表组件的位置进行计算,否则会出现偏移;第二,当手指滑动时,浮层要跟随更新,并且当手指离开时,浮层要自动消失,这个状态管理用 ValueNotifier 非常合适。
我没有用 fl_chart 自带的 tooltip,因为它在 OpenHarmony 上的动画在弱设备上有掉帧问题。自己画一个半透明的圆角卡片展示数据,反而更可控,性能也好。
6. 实际运行效果与 OpenHarmony 适配自查
6.1 设备适配结果概览
我用标准系统镜像在 RK 开发板上跑通了这套应用,从打开到记录加油,再到查看图表,操作流畅度符合预期。数据方面,表格能正确读写,多次安装卸载后数据不会丢失。图表渲染也稳定,没有白屏和错位。
最终验证时我特别关注了三个点:
- 应用安装后首次启动,数据库能否正常创建。
- 杀掉进程重启后,历史记录是否还在。
- 连续消费数据和绘制图表,内存是否衰减。
实测结论是:除了第一次冷启动时图表模块加载稍慢(大概多 200ms),其余运行状态很平稳。
6.2 需要注意的 ArkTS 与 Flutter 交互边界
OpenHarmony 工程里的 ArkTS 侧代码不要动太多,关键就是入口 ability 和资源路径。除非你的需求必须调用 OpenHarmony 的独特系统能力,否则保持纯 Flutter 侧逻辑,开发效率最高。
我朋友在某公司也做过类似迁移,他一开始为了追求性能,把数据库读写全部搬到 ArkTS 侧,然后用 MethodChannel 和 Flutter 通信,结果在丢线程调度后出了很多数据竞争问题,最后又搬回 Dart 侧,反而更稳定。我的建议是:不是系统级能力调用,就不要跨语言。
6.3 常用检查清单
- Flutter SDK 必须是 flutter_flutter 分支,不能在纯 Flutter 里强行构建 ohos 目录。
- sqlite3 的库加载路径必须在真机上验证,模拟器不一定有相同的库路径。
- fl_chart 的版本建议锁定在 0.66 左右,高版本可能有与我用的 Flutter 版本不兼容的问题。
- 路径获取必须通过原生 ability,不要用 Directory.current。
- 图表数据为空时必须做 UI 兜底,否则在弱设备上可能闪退。
7. 常见问题排查记录
7.1 构建阶段常见报错
- 报错:
Unsupported operation: Platform._operatingSystem,这通常是因为你在 Dart 侧用到了dart:io的 Platform 类,但 Flutter 在 OpenHarmony 上的某些嵌入模式没有返回正确的平台值。解决方法是在代码里避免依赖 Platform 判断,而是优先使用defaultTargetPlatform或干脆写一个环境判断分支。 - 报错:
Could not resolve jit pack,大多数情况下是你把 Android 的 Gradle 配置带入了 ohos 构建。需要清理项目里的 android 目录状态,确保flutter build ohos用的配置是基于 ohos 目录的。
7.2 运行阶段问题
- sqlite3 数据库打不开:检查最终的数据库文件路径是否在可写目录。如果直接放到 /data/public/xxx,可能会被权限拦截。
- 图表不显示:把图表的数据加载延迟到页面渲染完成后,用
Future.delayed或者postFrameCallback触发 initialLoad,然后再 setState。 - 中文字体乱码:OpenHarmony 上 Flutter 默认字体通常内置了中文,但如果你用了自定义的 fontFamily,注意打包时要在 ohos 工程里配置好字体文件路径。
7.3 性能调优技巧
在 OpenHarmony 设备上,我做了两件提升图表性能的事:第一,把图表的数据点抽稀。如果记录超过 100 条,只取最后 100 条展示,避免 Canvas 绘制过多点导致卡顿。第二,把图表的duration动画参数调成 0,禁用默认的动画插值,因为在弱设备上动画反而带来视觉上的卡顿体验,直接跳变更干脆。
数据抽稀的算法很简单:按索引等间距抽取,而不是随机抽取,保证趋势形态不变。目前我的记录还没超过 200 条,但提前做了这个处理,后续增长无忧。
8. 后续扩展与个人经验
做完油耗追踪器后,我在这个骨架基础上扩展了一个“保养提醒”模块,其实就是在加油记录之外新增一张保养记录表,设定时间或里程周期。当你新增一条加油记录时,检查当前里程是否超过了上次保养里程,如果超过了预设间隔,就给用户一个提示。整个扩展没有改动图表逻辑,只增加了查询逻辑和提醒列表页面。
这套项目的价值不在代码量,而在于证明了 Flutter 在 OpenHarmony 上可以承载完整的工具类应用。从数据库读写到图表渲染,只要避开原生插件依赖,剩下都是纯 Dart 的活,只有少数几个点需要和 ArkTS 原生侧打通,其他完全可以跨平台复用。如果你也想做类似迁移,建议从体积小、逻辑集中的工具类 App 着手,不要一上来就做带推送、地图这种强原生依赖的项目,否则光是插件适配你就能磨一个月。
最后说一个小技巧:如果你在 OpenHarmony 上调试 Flutter 应用时发现热重载不生效,试试先手动停止应用再重跑flutter run。这是因为 flutter_flutter 分支在某些版本上和 OpenHarmony 的增量部署机制配合得不够好,全量重跑能保证状态干净。我后来都是用这个方式,基本没再遇到奇奇怪怪的热更新问题。