基于Flutter与OpenHarmony的油耗追踪器设计与实现 开车的人多少都有过这种经历仪表盘上的瞬时油耗看着挺美一箱油跑完一算实际油耗和表显差出去一两升。想看真实油耗最靠谱的方法就是老老实实记录每一次加油数据然后用里程差去除加油量算出来的才是实际水平。这个项目就是干这个的——用 Flutter 在 OpenHarmony 设备上从零做一个油耗追踪器名字叫 FillUp工程名是 flutter_for_openharmonyFillUp。它要解决的核心问题很简单记录每一笔加油的里程、油量、金额自动算油耗再把一段时间的趋势画成图表让你一眼看出油耗是涨了还是降了。我做完这个项目最大的感受是油耗计算本身不难难点都在“数据怎么记准”“趋势图怎么画得有意义”这些细节上。这篇文章会把我的完整实现思路、踩过的坑、以及最后跑通的方案都整理出来。如果你正好想在 OpenHarmony 上跑 Flutter 应用或者想做一个类似的工具类 app可以直接按这个路径复现。1. 项目背景与整体设计思路1.1 为什么选 Flutter 做 OpenHarmony 应用OpenHarmony 应用的主流开发方式是 ArkTS也就是类 TypeScript 的声明式 UI 写法。但如果你手里已经有一套 Flutter 代码或者团队更熟悉 Dart那完全可以用 Flutter 的 OpenHarmony 适配分支来开发。Flutter 的渲染引擎是自绘的UI 不依赖系统原生控件所以在 OpenHarmony 上跑出来的界面和 iOS、Android 上几乎一致这一点在跨端应用里非常占优势。选 Flutter 还有一个实际原因开源社区里现成的图表库、状态管理库大部分是纯 Dart 实现的不需要依赖平台原生代码只要 Flutter 引擎能在 OpenHarmony 上跑这些库基本就能直接用。这就省掉了大量 ArkUI 里画折线图、柱状图的开发量。FillUp 的核心数据展示是趋势图图表库的可用性直接决定开发效率。1.2 FillUp 的功能范围与模块划分这个项目一开始就没打算做复杂一个工具类 app 最重要的是把核心闭环走通。功能范围锁定在四块记录加油数据日期、当前里程、加油量、金额、是否加满这几个字段缺一不可计算油耗用“本次里程减上次里程”得到行驶距离再用“本次加油量除以行驶距离”算出百公里油耗历史记录列表按时间倒序展示每一次加油记录方便核对和修改油耗趋势图用折线图展示最近 N 次油耗变化配合一个平均线直观反映油耗走势模块划分上数据层负责读写和计算状态层负责把数据变化通知到 UI展示层就只管渲染。这个分层在项目初期看起来有点重但当你要在列表、详情、图表三个页面共享同一份数据时就知道它的价值了。1.3 数据模型设计——油耗记录的核心字段数据模型是整个项目的地基。我当时设计字段的时候反复改过几版最后稳定下来的结构是这样字段类型说明idString唯一标识用时间戳生成dateDateTime加油日期用于排序和时间维度聚合odometerint当前里程数单位 km必须是整数记录volumedouble加油量单位 L保留 1 位小数amountdouble加油金额单位元可选字段isFullTankbool是否加满这是油耗计算的关键标记noteString备注比如“跑高速去了”“换了新机油”为什么一定要单独存 isFullTank因为油耗计算的准确度完全取决于有没有“加满”这个锚点。只加 50 块钱的油、没加满就跑那本次加油量不能反映完整的行驶消耗用它去算油耗会得出吓人的数字。这个字段我在后面计算逻辑里还会细讲。2. 开发环境搭建与工程配置要点2.1 环境准备与版本对齐OpenHarmony 的 Flutter 开发有个绕不过去的点不能直接用 google 官方的 Flutter SDK要用 OpenHarmony 社区维护的适配分支。这个分支有对应的仓库和版本管理我这次用的是当时最新的稳定版本。环境准备包含这几件事安装 OpenHarmony 官方的 IDE 工具主要是用来编译打包 OpenHarmony 侧的 HAP 产物下载指定版本的 Flutter 适配 SDK并配置 PATH 环境变量安装 OpenHarmony SDK包括 API 版本对应的工具链执行 flutter doctor 确认环境特别要检查 OpenHarmony 相关的状态是否正常版本对齐是最大的坑。Flutter 适配分支的版本和你本地的 Dart SDK、IDE 版本必须匹配否则编译到一半会报各种奇怪错误。我建议直接用文档里推荐的组合版本不要追求最新版稳定性优先。2.2 工程改造——从 Flutter 标准工程到 OpenHarmony 工程一个标准的 Flutter 工程默认包含 android、ios、web 这些目录但 OpenHarmony 的工程需要单独配置。具体做法是先创建一个普通 Flutter 项目然后在项目根目录下生成 OpenHarmony 工程目录把 flutter 模块作为依赖嵌进去。实际操作中几个容易出错的地方OpenHarmony 工程的包名要在配置里单独声明和 Flutter 工程的 applicationId 对应起来Flutter 的 Dart 入口要在 OpenHarmony 工程里显式声明类似 Android 的 MainActivity 配置编译产物是 HAP 而不是 APK构建命令也不同不能直接用 flutter build apk第一次跑通构建流程会浪费不少时间因为报错信息可能指向底层工具链不太好定位。我的经验是先跑一个空工程确认基础链路通了再逐步加入业务代码。千万不要一上来就把整个项目放进去否则出了问题都不知道是哪一步环境没配对。2.3 依赖选型与兼容性排查Flutter 生态的第三方库多但并不是所有库都能在 OpenHarmony 分支上跑。区分的方法很简单看这个库是否依赖了原生插件。纯 Dart 实现、只靠 Flutter 框架 API 的库基本都能直接兼容而依赖平台通道调用 iOS/Android 原生功能的库就可能需要 OpenHarmony 适配版本。FillUp 用到的核心依赖fl_chart纯 Dart 绘制图表用作趋势折线图兼容性没问题provider纯 Dart 状态管理没有任何原生依赖intl日期格式化纯 Dartpath_provider这个是有原生插件依赖的但 openharmony 社区有适配或者可以选择直接用 dart:io 拿目录排查依赖兼容性有个高效方法把依赖加进去后先跑一下 flutter analyze 和编译测试阶段就发现问题永远比上线后发现问题好。我遇到过某个库在 Android 上正常编译但 OpenHarmony 上因为原生方法缺失直接构建失败这种就要果断换掉或者找替代方案。3. 油耗计算逻辑与数据管理实现3.1 油耗计算公式与加满判断油耗计算的核心公式就是百公里油耗 本次加油量 /本次里程 - 上次里程× 100。这个公式看着简单实际落地有几个场景要处理清楚场景一首次记录。第一次加油时没有“上次里程”没法算油耗。这时候只保存记录不显示油耗值。用户看到的是“—”而不是一个错误的数字。场景二加满才参与计算。刚才提到isFullTank 这个字段很关键。只有当前记录是加满状态且上一次加满记录存在时这次加油才能参与油耗计算。中间如果夹了几次没加满的记录计算时要自动跳过拿当前加满记录去匹配上一次加满记录。这样可以避免预付款小额加油、中途补油对数据的污染。场景三异常数据剔除。有时候里程数记错了或者加油量异常算出来的油耗低到 1L/100km 或者高到 30L/100km。这种数据如果直接上图表会把整个趋势的尺度拉垮。我加了一个过滤规则油耗在 3~25 L/100km 之外的数据标记为异常不进入趋势计算但原始记录仍然保留。还有一个容易被忽略的细节加油量要以实际升数为准不是以金额为准。在油价波动的时候同样 200 块钱加的油量可能差好几升。记录金额只是为了做消费统计计算油耗必须用升数。3.2 本地存储方案——从 SharedPreferences 到 JSON 文件Flutter 应用存数据大家第一反应就是用 shared_preferences。但这个库在 OpenHarmony 上不是开箱即用的需要额外的适配插件而且 SharedPreferences 定位是存轻量配置存几十条还好存几百条加油记录反复读写就有点别扭了。我最终选的是 JSON 文件存储理由很实际油耗记录是典型的低频写入、整量读取场景每次加油才新增一条记录偶尔修改一条。用文件存 JSON 数组启动时一次性加载进内存修改后整个序列化写回逻辑简单而且完全不依赖平台通道天然兼容 OpenHarmony。存储路径通过 path_provider 拿应用文档目录如果这个库在目标平台有问题也可以用 dart:io 直接拼一个相对路径指向应用沙箱目录。我自己最后就是用文件存储实现的数据文件结构大概是[ { id: 202401151032, date: 2024-01-15 10:32:00, odometer: 15230, volume: 42.5, amount: 331.5, isFullTank: true, note: } ]每次写入前先深拷贝当前列表新增记录后重新排序并写回文件。为了防止写一半断电导致数据损坏我用了“先写临时文件再改名替换”的方式这在移动设备上虽然不是必须的但多一步操作能避免很多意外。3.3 状态管理——用 Provider 串起数据流数据层和 UI 层的连接我用的是 Provider。选择它的原因不是因为它功能多而是因为它足够简单没有代码生成没有额外编译步骤纯 Dart 实现兼容性最稳。状态管理的设计是三个核心类一个用来加载和保存数据文件的仓库类Repository一个持有记录列表、负责增删改查和油耗计算的模型类一个暴露给 UI 的 ChangeNotifierUI 层的页面会监听模型类的变化。每次新增加油记录后模型自动重新计算所有油耗数据并通知图表页面刷新。这里有个小技巧模型内部分两组数据一组是原始记录一组是计算后的油耗点。计算逻辑封装在模型里UI 永远只拿结果不参与运算。这样如果哪天要改成 SQLite 存储只需要改仓库类计算和 UI 完全不用动。4. 油耗趋势图表的实现与优化4.1 图表方案选型——为什么用 fl_chart趋势图是 FillUp 的门面也是这个项目里最有技术含量的一部分。图表方案我对比过两个一个是 fl_chart另一个是自绘 CustomPaint。自绘听起来很酷但其实工作量不小。坐标轴、刻度、折线、交互点击、tooltip 提示这些全部自己绘制光是对齐逻辑就要写几百行更别提 touch 事件的处理。fl_chart 虽然配置项多但它把这些能力都封装好了我只需要提供数据点和样式配置。fl_chart 是纯 Dart 写的绘制过程全部走 Flutter 的 Canvas API不依赖任何平台原生代码。这一点决定了它在 OpenHarmony 上兼容性极好不用像某些图表库那样必须等适配版本。实际跑下来图表渲染性能和 Android 上没有明显差别。4.2 趋势数据聚合与平滑处理直接拿每一次油耗数据画图效果往往很差。原因有两个一是数据点不连续。因为用户可能隔几次加油才加满一次中间的数据不参与计算导致时间轴上分布不均匀。二是单次误差明显。跳枪、加油站油枪计量差异、路况变化都会让单次油耗波动很大画出来是一条锯齿很粗的线看不出真实趋势。我的处理方案是双轨并行原始油耗折线展示每一次计算出的实际油耗用浅色绘制移动平均趋势线对最近 5 次有效油耗做滑动平均用深色绘制滑动平均的计算方式Listdouble movingAverage(Listdouble values, int window) { if (values.length window) return values; final result double[]; for (int i 0; i values.length; i) { int start (i - window 1).clamp(0, values.length - 1); double sum 0; int count 0; for (int j start; j i; j) { sum values[j]; count; } result.add(sum / count); } return result; }加上移动平均之后趋势线明显平滑了能看出油耗是 “持续变好”还是“稳定变差”这才是趋势图应该回答的问题。4.3 图表交互与展示细节fl_chart 的 LineChart 配置比较啰嗦但可定制性强。我在实现趋势图时重点调了几个地方Y 轴范围自适配。油耗数据的波动范围不大可能在 6.5 ~ 9.5 之间。如果 Y 轴从 0 开始折线会变成一条贴近顶部的平线看不出任何变化。所以我把 Y 轴最小值设为“所有数据点最小值下浮 1”最大值设为“最大值上浮 1”这样趋势变化才能被视觉放大。Tooltip 显示关键信息。点击折线点的时候弹窗展示“日期、里程、加油量、计算油耗”方便用户对照原始记录排查数据异常。平均油耗虚线。在图表上画一条平均油耗的虚线用户一眼就能看出当前油耗是高于平均水平还是低于平均水平。这条线的实现方式是再加一个只有两个点的 LineChartBar颜色用浅灰模式设为虚线。中文文本渲染处理。图表的横轴标签和 tooltip 都包含日期在 OpenHarmony 上中文渲染一般没问题但有一个情况要注意如果系统字体或 textScale 设置了特殊值可能导致标签溢出或者错位。我在图表外层用 FittedBox 包了一层防止极端字体设置下的布局崩坏。5. 常见问题与调试经验实录5.1 平台适配中的典型坑OpenHarmony 上跑 Flutter由于平台差异有几个问题是标准 Flutter 开发中很少遇到的。字体渲染差异。同样的文本OpenHarmony 上中文默认字体和 Android 不完全一样。如果 UI 里用了固定行高或者固定宽度的组件文字渲染变宽后可能被截断。解决思路是避免硬编码行高多用 IntrinsicWidth 或者给文本足够的弹性空间。安全区域问题。OpenHarmony 设备的底部导航条、状态栏高度和 Android 不同Scaffold 默认的 SafeArea 行为在部分设备上表现不一致。我是在页面最外层包了 SafeArea并且通过 MediaQuery 动态获取边距避免硬编码。剪贴板、相册等平台能力。这类能力依赖平台通道OpenHarmony 适配情况参差不齐。FillUp 的输入都是手动填写的数字没有依赖这类能力所以没踩到坑。但如果你的应用要用到图片选择、生物识别之类的能力一定要提前调研 OpenHarmony 原生侧的适配情况。5.2 图表渲染性能优化趋势图在数据量少的时候没感觉但记录攒到两三百条之后每次增删改都 rebuild 整个图表还是会有一点点卡顿。我做了一次性能优化效果很明显把图表拆成独立组件并用 selector 精准监听。Provider 默认在 notifyListeners 的时候所有监听者都会 rebuild。FillUp 的记录列表、统计卡片、趋势图都监听同一个模型意味着每次加一条记录三个模块全部重建。我引入了 Selector让图表组件只监听“油耗点列表”这个字段的变化记录列表只监听“原始记录列表”的变化把重建范围降到最小。避免在 build 方法里做重复计算。刚开始我把移动平均的计算直接写在图表组件的 build 方法里每次 rebuild 都重新算一遍。后来把计算挪到了模型层在 setState 的时候只算一次UI 直接缓存结果性能提升明显。关闭不必要的动画。fl_chart 默认有隐式动画数据变化时折线会有一个平滑过渡效果。这个效果在演示的时候很好看但数据频繁刷新时会占用不少资源。我在设置里把动画时长降到了 300ms并且只在初次加载时启用动画后续数据更新直接用 immediate 模式。5.3 数据准确性排查技巧使用过程中用户反馈最多的问题是“油耗数据不准”。排查下来绝大多数情况不是计算逻辑错了而是录入数据的问题。跳枪会导致油量偏高。加油员为了凑整金额跳枪后会继续补油这样可以多加几十毫升到一两升不等。Add 记录的时候我加了“手动修正”按钮用户可以填写实际想要计入的加油量。这个修正只影响计算不影响原始显示。里程数录错。仪表盘读数看串了或者小数点后几位看错了结果算出来的油耗会非常离谱。我在保存前加了合理性校验如果计算出的油耗超出 3 ~ 25 L/100km 的范围会弹窗提醒“油耗异常请检查里程或油量”但不强制阻止保存。这个提醒能拦截大部分误录又不会让用户觉得被冒犯。时间顺序错乱。如果用户上次记录的里程是 20000本次录成了 18000计算出来是负油耗。我在保存前检查里程单调性发现“本次里程小于上次里程”时就提示用户并让用户确认是否强制保存。因为有些用户会用两辆车交替记录这种情况下里程回退是正常的需要人工判断。5.4 调试与打包的注意事项开发期调试用的是热重载但 OpenHarmony 上和标准 Flutter 的热重载不完全一样有时候改了原生相关配置必须重新构建 HAP 才能生效。纯 Dart 代码的修改热重载基本可用但和平台通道相关的能力比如文件路径变更建议直接全量重启验证。打包阶段的几个要点HAP 的签名配置在 OpenHarmony 工程里单独配置和 Flutter 的签名配置是两回事打包前要在 IDE 里配置好自动签名否则安装到真机会失败真机调试时通过 IDE 安装 HAP网络端口映射方式跟 Android 不完全一样不要直接用 adb 思路去套我在项目里实测下来编译一个 release 版 HAP 的时间比 Android APK 要长一些主要耗时在 OpenHarmony 侧的资源编译和打包。建议把构建流程脚本化避免每次都手动点 IDE 菜单。6. 个人体会与经验总结这个项目做完我自己收获最大的一点是跨端框架的价值不在于“写一套代码到处跑”这句话本身而在于它的生态和工具链能不能在你需要的时候恰好支撑到。Flutter 画图表、管状态、写文件这些基础能力在 OpenHarmony 上全都跑通了这让我后续做其他 OpenHarmony 应用时有了很大信心。也分享一个测试心得油耗数据这个场景非常适合作时间序列类应用的练手题材。数据点不多但涉及增删改查、聚合计算、图表可视化、异常数据过滤麻雀虽小五脏俱全。如果你想研究 Flutter 在 OpenHarmony 上的能力边界我建议也找一个类似“量级适中但有真实计算逻辑”的场景去做比如记账、体重记录、饮水打卡都会比写 demo 更深入理解整个框架。最后留一个扩展方向FillUp 目前的数据都存在本地文件里如果后续想做得更深可以考虑接入云同步把记录推送到远端在 Web 端做数据分析和导出。这个功能对用户的长期留存提升会很明显。而且本地存储和云端同步的数据层是可以平滑切换的只要把仓库类的接口保持一致就行。