尧图精选

Flutter在OpenHarmony上的油耗追踪器开发全解析

🕒 发布时间:2026/10/2 3:20:31 📁 来源:尧图网络
做这个flutter_for_openharmonyFillUp项目起因很朴素我自己有台车每次加油都是靠手机备忘录记里程、记金额月底想算一下真实油耗得翻开一堆记录手动算费劲还容易漏。后来正好在搞 Flutter 跨端开发又接触了 OpenHarmony 设备就想着干脆写一个油耗追踪器 App把记录、统计、详情展示一条链路打通。项目名字里的FillUp就是“加满一次油”的意思记录详情则是整个应用里信息密度最高、也最考验交互设计的地方。这篇文章会把整个实战过程拆开讲从 OpenHarmony 上的 Flutter 环境搭建到数据模型和油耗计算再到列表页、记录详情页的具体实现以及 Flutter 和 OpenHarmony 原生层通过 EventChannel、MethodChannel 通信的细节。过程中遇到的问题和排查思路我也会一并整理出来给想在这个方向动手的朋友做个参考。无论你是刚开始接触 Flutter 跨端开发还是已经在 OpenHarmony 上踩坑这篇文章都值得看完实操。1. 项目背景与整体设计1.1 项目的真实痛点油耗追踪这个场景表面上只是一个“记录 计算”的小工具但真正做起来会碰到几个很实际的需求点。第一是记录要尽可能简单。每次加油用户关心的核心数据就几个当前总里程、这次加的油量、花费金额、油价以及一个“是否加满”的标记。但是“简单”并不意味着字段少反而要处理好自动计算和手动修正之间的平衡。比如油价可以按金额除以油量自动反推但有些用户喜欢手动录入油价系统应当允许覆盖。第二是计算逻辑要正确。油耗不是“单次加油量除以单次里程”这么简单而是要看两次加满之间的消耗关系。如果上次没加满、这次加满了计算出来的数据就没有参考意义。所以数据模型里必须记录isFullTank计算时只选取连续两次“加满”的记录。第三是记录详情的展示。用户点进一条记录不只是想看到“7.2 升/百公里”这个数字他还想知道这次加油和上一次相比平均油耗是涨了还是跌了最近几笔的趋势是不是稳定每公里的成本大概多少。这些信息需要在一个页面里分层排布既要直观又不能堆砌。1.2 技术选型与总体架构选择 Flutter 而不是原生开发主要有两个原因一是这套代码以后可以平移到其他端不想被单一平台绑死二是我对 Dart 和 Flutter 的 UI 开发效率有把握特别是列表和图表这类需要频繁改动的页面Flutter 的组件化写法比原生控件要顺手很多。OpenHarmony 上的 Flutter 生态和传统 Android 有一点差别。Flutter 官方发布版本主要支持 Android、iOS、Web、桌面而 OpenHarmony 需要依赖社区维护的flutter_for_openharmony分支引擎。这个分支的适配思路是Flutter 的 Dart 层和渲染层保持不变底层 Platform 通道替换成 OpenHarmony 的 API 实现。所以业务代码可以尽量用标准 Flutter API但涉及原生能力的部分要针对 OpenHarmony 重新写插件适配。整体架构我拆成了三层UI 层Flutter Widget负责列表、详情、图表、表单。逻辑层Cubit 状态管理负责油耗计算、数据筛选、页面状态同步。数据层本地存储 原生通道负责记录的持久化、CSV 导出、设备数据读取。这样分层的好处是后面如果要把数据源从本地文件换成 SQLite或者接入车辆 OBD 设备只改数据层UI 层不用动。这也是我在项目中期切换数据存储方案时没有重写页面的原因。2. 环境搭建与工程初始化2.1 准备 OpenHarmony 侧的 Flutter 开发环境如果你之前只配过标准 Flutter 环境第一次弄 OpenHarmony 分支会有种“回到了五年前”的感觉不能用 flutter 官方命令直接创建鸿蒙工程要先准备编译好的引擎产物。我的操作流程是这样从 OpenHarmony 相关的 gitee/社区仓库拉flutter_for_openharmony分支代码。按照 README 要求用 DevEco Studio 打开项目里的ohos目录等它自动同步依赖。配置本地 Flutter SDK 路径和 Dart SDK 路径注意不能用官方 SDK 直接跑必须切换到对应的 fork 版本。执行构建命令生成 OpenHarmony 可用的flutter.har和引擎 so 文件。这里特别想提醒一句环境变量里PATH的顺序很重要。我一开始因为系统里同时存在官方 Flutter 和 fork Flutter导致命令行执行flutter doctor时识别的是官方版本构建时各种报“artifact mismatch”。后来把所有命令都换成绝对路径或者单独开一个终端加载鸿蒙环境的env.sh问题才消失。2.2 工程目录结构与原生侧接入项目创建好之后目录结构是典型的 Flutter 工程混合原生工程flutter_for_openharmonyFillUp/ ├── lib/ # Dart 业务代码 ├── ohos/ # OpenHarmony 原生工程 │ ├── entry/src/main/ets/ # ArkTS 能力封装 │ └── entry/src/main/resources/ ├── pubspec.yaml └── build.yaml原生侧接入的核心是把 Flutter 引擎挂到 ArkTS 的页面生命周期里。OpenHarmony 的 Flutter 适配包提供了一套FlutterAbility或FlutterUIAbility之类的容器你只需要在main_pages.json里注册对应的页面然后在onPageShow里调用loadFlutter方法加载你指定的 Dart 入口。我建议直接使用容器容器不要自己用PlatformView去嵌一个 FlutterView除非你有强烈的原生混合需求。填坑经验是自建嵌入方式对输入的焦点处理、键盘弹起、生命周期转发都不如现成容器稳定尤其是详情页里有文本输入框时键盘会遮挡输入框排查起来非常痛苦。2.3 依赖管理与首次构建的坑pubspec.yaml里的依赖不要盲目使用最新版。我遇到过几个纯 Dart 的包没有适配 OpenHarmony结果在编译期没问题运行期直接报 MissingPluginException。后来我养成了一个习惯每个依赖都去查一下是否有人提交过 OpenHarmony 平台的适配 PR或者是否有 fork 版本。首次构建最耗时的是引擎产物下载 OpenHarmony 预编译的 engine artifact 时网络不稳会导致解压失败。解决方案是先手动下载对应的flutter_ohos_engine.zip放到本地~/flutter_ohos/engine_cache再设置环境变量让构建脚本优先读缓存。这一步省下的时间够我写完整套列表页逻辑。3. 数据层设计与油耗计算3.1 记录字段与业务模型数据模型是整个项目的地基。我设计的FuelRecord字段如下字段类型说明idString主键用时间戳随机数生成dateDateTime加油日期odometerdouble当前总里程单位 kmlitersdouble本次加油量单位 Lamountdouble本次消费金额单位元pricedouble油价优先手动填写否则自动计算isFullTankbool是否加到跳枪noteString备注比如“跑高速居多”createdAtDateTime创建时间Dart 模型里我加了两个扩展字段costPerKm和avgFuelConsumption但这两个字段不持久化每次读取列表时动态计算。这样做的原因是避免数据不一致比如用户改了里程或油量旧的平均值没有重算展示内容就会失真。3.2 本地存储方案为什么我没有一开始上数据库按理说记录类应用应该用 SQLite但 OpenHarmony 上直接引sqflite会有原生依赖适配的问题社区虽然有移植版本但当时我评估了一下项目的数据量最多几千条用文件存储完全够而且还能避免构建时引入系统 SQLite 的依赖版本冲突。我最终采用的方案是一个 JSON 文件全量保存 内存缓存。class LocalRecordStore { FutureListFuelRecord loadAll() async { final dir await getApplicationSupportDirectory(); final file File(${dir.path}/fuel_records.json); if (!await file.exists()) return []; final jsonArray jsonDecode(await file.readAsString()) as List; return jsonArray.map((e) FuelRecord.fromJson(e)).toList(); } Futurevoid saveAll(ListFuelRecord records) async { final dir await getApplicationSupportDirectory(); final file File(${dir.path}/fuel_records.json); await file.writeAsString(jsonEncode(records.map((e) e.toJson()).toList())); } }写入策略上做了一点优化不是在每次增删改后都全量写入而是把写文件操作放到一个队列里延迟 300ms 合并写入。用户在详情页连续编辑多个字段时只触发最后一次磁盘写入实测下来流畅度提升非常明显。全量 JSON 的方案不适合海量数据但对个人记录场景是足够稳的。顺带一提日常记录数据同步可以用shared_preferences存一些轻量配置比如“当前仪表盘里显示的单位”但真正的记录数据不要塞进 preferences它的 get/set 会把整个文件读一次数据大了之后不仅慢还可能触发系统级 IO 异常。3.3 油耗计算逻辑与边界处理油耗计算看起来简单但边界情况很多。我的计算逻辑只保留了一个核心公式double calculateConsumption(FuelRecord current, FuelRecord previous) { if (!current.isFullTank || !previous.isFullTank) return 0; double distance current.odometer - previous.odometer; if (distance 0) return 0; return current.liters / distance * 100; }这个公式的前提是“本次加满时油箱里的油量等于上次加满时的油量”所以previous也必须是isFullTank为 true 的记录。实际上也可能存在燃油消耗率差异导致误差但作为个人记录工具这个精度足够。另外还要处理几个很隐蔽的问题用户跳过一次加油记录那中间一段的油耗会并入下一次计算可能偏高。我的做法是详情页显示计算时在旁边标注“参考区间3月15日 - 3月28日”让用户自己判断。里程表会清零有些车有 Trip A/B。我在录入时增加了一个isOdometerReset标记位。如果用户在详情里勾选了这个标记计算时会用油量除以一个手动输入的区间里程而不是直接做差值。金额和油量都可能为 0。新增记录时我会校验liters 0 amount 0避免出现脏数据把平均油耗拉低。4. 列表页与记录详情的核心实现4.1 列表页下拉刷新与动态卡片列表页是整个 App 最先展现的部分。我没有用表格式的列表而是每条记录一个卡片卡片上首屏信息只有四个日期、加油量、金额、百公里油耗。油耗如果超过 9 升数字标红低于 6 升标绿中间值用默认色。下拉刷新是 Flutter 里最常用的交互之一直接使用RefreshIndicator包一层ListView就完事。但这里有个比较隐蔽的问题RefreshIndicator的onRefresh回调需要返回一个 Future如果你在回调里只做了内存数据的 setState没有等待数据加载完成下拉的动画就会一闪而过看起来非常敷衍。我的做法是Futurevoid _onRefresh() async { Completervoid completer Completer(); _recordCubit.refresh().then((_) completer.complete()); await completer.future; }如果刷新时还要通过 EventChannel 向原生请求蓝牙 OBD 设备的新数据那要把原生回调结果也纳入这个 Completer 的等待逻辑所有数据源都确认返回后再结束刷新动画用户体验才完整。列表页的另一个重点是无数据状态。空卡片不要只放一句“暂无记录”我做了三个按钮手动新增、导入示例数据、打开本地帮助页。这一细节能显著降低用户刚下载 App 时的挫败感。4.2 详情页信息分层展示记录详情页是标题里的重头戏。我的设计分成四个区域摘要区最近一次油耗、总费用、平均油价、累计里程。记录信息区把一条记录的完整字段展示出来包括备注、是否加满每条边上有一个小的编辑图标。对比区和上一次记录相比油耗增减了多少。用箭头 颜色来表达趋势。趋势图区最近十次记录的折线图。在 Flutter 里页面之间的数据传递我走了 Cubit而不是直接在构造函数里塞一个 record 对象。为什么因为详情页可能从不同入口进入列表点击、首页快捷入口、搜索结果。如果每个入口都传对象入口多了会膨胀而且从详情页返回列表时列表的数据可能已经变了让详情页通过同一个 Cubit 状态去取数据能保证一致性。具体实现是详情页在initState里调用_recordCubit.fetchById(recordId)然后使用BlocBuilder监听状态class RecordDetailPage extends StatelessWidget { final String recordId; // 从处中读取 recordId }如果详情页刚打开时状态还没加载完先展示一个骨架屏而不是转圈。骨架屏的好处是让用户感觉内容即将出现在弱网环境下尤其重要。详情页还有一个容易忽略的点日期显示。2025-03-16 14:30这种完整时间太长我做了分组显示今天显示“今天 14:30”昨天显示“昨天 14:30”一周内显示“周三 14:30”更早的显示日期。这些细节很琐碎但对使用体验影响极大。4.3 趋势图用 CustomPainter 自己画记录详情页里的趋势图我原本想引用第三方图表库但 OpenHarmony 的兼容性让我犹豫了一下最后干脆用CustomPainter自己画了一个折线图。原因有二第三方图表库通常依赖大量 canvas 操作虽然 Flutter 的 canvas 和平台无关但一些库内部用了文本测量、手势冲突处理在 OpenHarmony 的引擎分支上表现不稳定。油耗趋势图的数据结构很简单只有 x 轴时间、y 轴油耗数值自己写的成本不超过 200 行。绘制思路class FuelTrendPainter extends CustomPainter { final Listdouble values; // ... override void paint(Canvas canvas, Size size) { // 把 values 映射到 canvas 坐标 // 先画背景网格线 // 再画折线和圆点 // 最后在最高点和最低点旁边标注数值 } }有几点经验值得分享如果相邻两次记录的油耗差距太大比如差 3 个油折线会异常陡峭。我加了平滑插值使用简单的 Catmull-Rom 算法图表看起来自然很多。图表区域的底部一定要留出足够空间否则屏幕底部的手势条会遮挡最后一个点。不要用RepaintBoundary包裹整个长列表只建议包趋势图区域避免每次滚动都触发全页面重绘。4.4 编辑、删除与页面状态保持编辑页和新增页我复用了同一个表单组件区别只在初始值。删除操作做了两步确认防止误触。这些常规操作里真正让我踩坑的是页面上“状态保持”的问题。Flutter 里用Navigator.push从列表页进详情页再退回来时列表页的 State 默认是保留的因为路由栈里列表页还在。但是如果你在详情页修改或删除了某条记录返回列表后列表必须刷新。很多人会直接Navigator.push后再await路由的返回值然后 setState这在数据量小的场景够用但项目里如果列表页使用了AutomaticKeepAliveClientMixin保持滚动位置这种 await 再 setState 的方式有时候会因为 context 生命周期问题或者状态异步更新顺序出现 UI 没有刷新的情况。我的最终方案是所有记录的增删改都走 Cubit由 Cubit 统一更新 state。列表页的 State 用BlocListener监听状态变化只要数据版本号变化就重新加载列表。这样就不需要关心路由返回时的状态同步了也不会出现 Navigator 切换页面后丢失状态的问题。这个思路对任何需要跨页面共享数据的场景都适用。5. Flutter 与 OpenHarmony 原生能力交互5.1 MethodChannel调用原生能力Flutter 和 OpenHarmony 原生的通信主要有两种通道MethodChannel用于请求-响应式调用EventChannel用于事件流推送。在这个项目里MethodChannel 主要做了三件事导出 CSV 文件并调用系统分享。获取设备的唯一标识用于统计。检查并申请必要的存储权限。以导出 CSV 为例Dart 侧final channel MethodChannel(com.fillup.ohos/export); await channel.invokeMethod(exportCsv, {path: tmpPath, name: records.csv});ArkTS 侧通过注册 MethodChannel 处理逻辑。这里最需要注意的是线程切换原生侧实现onMethodCall时不要直接在主线程做文件 IO要放到子线程完成后使用result.success返回。我在实际开发中遇到过一个问题导出大 CSV 时主线程阻塞导致 Flutter 侧白屏了约十秒。原因是原生侧直接在 UI 线程执行了writeText。改成 TaskPool 后问题解决。5.2 EventChannel让原生消息驱动 Flutter 刷新EventChannel 是这次项目中后期加入的原因是我想接入车辆的 OBD 蓝牙模块让每次点火后自动同步当前里程数据到 App。虽然完整接入还需要硬件调试但 EventChannel 的通信框架我提前搭好了。原生侧向 Flutter 发事件的伪代码// ArkTS 侧 private eventSink: EventChannelSink | null null; this.eventChannel new EventChannel(com.fillup.ohos/obd); this.eventChannel.setEventSink((sink) { this.eventSink sink; }); // 收到硬件数据时 this.eventSink.success({ type: obdMileage, value: 12345.6 });Flutter 侧监听EventChannel(com.fillup.ohos/obd).receiveBroadcastStream().listen((data) { if (data is Map data[type] obdMileage) { _updateOdometer(data[value].toString()); } });使用 EventChannel 要注意消息量。OBD 设备可能每秒钟上报多条数据如果每条都直接更新 Cubit 状态并触发 UI 刷新既浪费又会导致页面卡顿。我在接入层做了节流只保留最近一次数据并且用debounce500ms 后才提交到状态层。另外要处理 Flutter 页面不在前台时收到事件的情况正确做法是只把数据写入一个临时缓冲区等页面可见后一次性呈现。5.3 PlatformView 与 HDI 扩展思路关于 Flutter 里的PlatformView如果是想在 Flutter 页面里嵌一个原生地图或原生相机预览它是一个备选方案。但我在油耗记录详情页里没有使用而是通过 MethodChannel 把地图静态图生成后返回给 Flutter 展示减少了原生视图和 Flutter 视图的混合渲染成本。OpenHarmony 的 PlatformView 适配目前还在完善中能不用尽量不用。如果你后续要读取车辆 OBD 的原始硬件数据就需要涉及 OpenHarmony 的 HDI 层。HDI 是硬件设备接口蓝牙或者 CAN 外设的数据会从 HDI 驱动层向上传递。对应用开发者来说一般不会直接碰 HDI而是调用系统蓝牙 API或者使用 Native 编写的中间库。但从架构上讲硬件事件最终通过 EventChannel 进入 Flutter 层这个链路是清晰的。6. 构建、打包与问题排查实录6.1 最常见的构建错误与处理构建 OpenHarmony Flutter 工程时最经典的一个报错就是依赖仓库里的 Gradle 版本和 DevEco Studio 内置版本不一致。报错信息五花八门有Could not resolve、有SDK location not found、还有java.lang.AssertionError。遇到这类问题先不要怀疑代码按这个顺序排查确认ohos工程的build-profile.json5里 SDK 路径是否有效。确认 fork Flutter 的缓存目录pub cache是否完整必要时删除后重新pub get。确认 Gradle 缓存没有残留损坏的 artifact手动打开 Gradle 缓存目录检查.part文件。用命令行执行一次flutter build hap --debug比 IDE 里报的信息更完整。我还遇到过编译期报resource not found的情况最后定位是ohos工程里的字符串资源文件编码从 UTF-8 变成了 GBK把文件重新转成 UTF-8 就解决了。这类问题很少在文档里提到但排起来很快建议优先检查资源文件和配置文件。6.2 运行期的状态与异步问题运行期最多的问题集中在这几个地方状态丢失前面提到过如果详情页直接把数据写在 State 里从别的入口打开会拿不到数据。后来所有记录详情的展示都改为通过 Cubit 按 recordId 拉取再配合Equatable做状态比较基本根治了。异步上下文泄漏详情页加载数据的 Future 完成后如果用户已经关闭了页面再调用context就会引发异常。我尽量把数据加载和 UI 状态分离数据加载通过 Cubit 内部处理页面只负责监听。Future 回调执行时机关于Future.then回调是不是放入微任务队列里的问题结论是Dart 的Future在完成之后注册的then回调会作为微任务被调度。这里要注意的是不要在then里做重量级操作比如写文件、解析大 JSON会阻塞微任务循环导致 UI 卡顿。我通常用一个小的compute或者isolate来做解析。下拉刷新卡住如果Completer永远不 complete刷新动画会一直转。这个问题常见于原生回调没有在超时后触发。我给所有原生调用都加了一个 5 秒超时超时后completeError并提示“刷新超时”。6.3 真机调试与性能优化OpenHarmony 真机调试时日志输出和 Flutter DevTools 的连通性偶尔不稳定。我个人比较常用的组合是业务日志用ohos/log输出到 DevEco Studio 的 Log 面板Flutter 侧的性能数据用 DevTools 在调试模式下观察。如果 DevTools 连不上先检查设备是否开启了“调试模式”然后检查 ADB 端口有没有被其他进程占用。性能方面记录详情页最耗时的操作有两块一是从 JSON 文件加载全部记录后进行排序筛选二是趋势图的绘制。前者我加了内存缓存只有档案文件发生变化时才重新解析后者通过只计算最近十条记录并把计算过程放在compute中绘制压力明显下降。另外列表页如果一次性渲染几百条卡片会有明显的首帧卡顿。我改用ListView.builder懒加载并给卡片加上const构造器。卡片中的日期格式化结果也做了缓存避免每帧重复计算。7. 一些想分享给后来者的经验7.1 我踩过的最值钱的一个坑整个项目里最值钱的坑不是技术问题而是“对跨端适配的敬畏”。我一开始心态是Flutter 写业务代码底层反正自绘OpenHarmony 上应该和 Android 差不多。实际开发中很多细节暴露了差距TextInput的键盘弹出高度在 OpenHarmony 上需要单独配置部分CustomPainter的混合模式在软件渲染时效果异常SystemChrome设置状态栏颜色在部分机型上不生效。这些问题单拎出来都不大但凑在一起会让你对“一次编写到处运行”产生深刻的重新认识。我的建议是重要的页面和交互尽量在第一个迭代就上真机验证不要等项目写完再集中适配。越早发现问题改造成本越低。比如键盘遮挡问题如果第一周就发现表单布局可能完全不一样。7.2 这个项目后续还能怎么扩展记录详情这块目前是单条记录的展示和趋势图。后面我打算做几个扩展按月汇总详情把一个月内所有记录聚合显示这个月的总加油金额、平均油耗、平均每日里程。车辆维度支持同时记录两辆车的油耗数据详情页可以切换车辆筛选。CSV 备份与恢复已经做了导出恢复功能即将开发把 JSON 文件通过文件选择器重新导入。如果你也想在 OpenHarmony 设备上做类似的数据记录工具我建议先从小而完整的闭环开始一条记录的上传、保存、展示、删除比一上来做复杂的统计图表要稳得多。数据闭环跑通后再逐步加可视化、加同步、加设备接入。最后再分享一个小技巧在 OpenHarmony 上调试 Flutter 的 UI 布局时如果发现某些 Widget 显示位置和 Android 有偏差优先检查是不是字体渲染差异导致的。OpenHarmony 默认字体和 Android 的默认字体在行高、基线对齐上有细微区别设置固定TextStyle.height或者给关键文本加上strutStyle能让布局稳定很多。这个坑我查了整整一个晚上希望你能少走弯路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →