☰
OpenHarmony上跑Flutter:油耗追踪器实战开发全记录
2026/9/30 3:30:23 网站建设 项目流程

1. 项目缘起:为什么要在 OpenHarmony 上跑 Flutter 做油耗追踪器

先说这个项目的来由。我一直有记录加油数据的习惯,每次加油顺手把里程、升数、金额记下来,几个月下来就能算出真实油耗和养车成本。市面上现成的油耗 App 不少,但大多绑定账号、有广告、数据还要上传云端,我只想要一个纯本地、打开就能看、界面清爽的小工具。

正好那段时间我在研究 OpenHarmony 上的跨平台开发,Flutter 对 OpenHarmony 的适配已经能跑起来了,就决定自己写一个。项目名叫 FillUp,核心场景就是随手记录加油,然后打开概览页就知道三件事:最近一箱油跑了多少、平均油耗是多少、这个月花了多少钱。

这个 App 的技术选型可能有人觉得绕:跑在国产操作系统上,用跨平台框架,做的是一个很小的工具类应用。但我的想法很直接,Flutter 在 OpenHarmony 生态里是少数能保证 UI 一致性和开发效率的方案,而油耗追踪器这种数据模型清晰、页面结构固定的应用,正好适合用来验证整个工具链是否可用。如果你也在研究 Flutter 和 OpenHarmony 的配合,或者想把手头的小工具迁移到 OpenHarmony 上,这篇实战记录适合你。

2. 工程环境:Flutter SDK 与 OpenHarmony 工程的联动配置

2.1 用 flutter_fluh 而不是官方 Flutter SDK

OpenHarmony 上跑 Flutter,第一步就有一个很关键的差异:你不能直接用 flutter.dev 下载的官方 SDK 来创建 OpenHarmony 工程。

OpenHarmony 社区维护了一个独立的 Flutter 仓库,叫 flutter_fluh,它有专门适配 OpenHarmony 平台的分支。你需要把这个仓库 clone 下来,切换到对应的版本分支,然后用这个 SDK 来执行 flutter create。

我用的是 flutter_fluh 的 OpenHarmony 3.x 版本分支,命令大概是这样的:

git clone https://gitee.com/openharmony-sig/flutter_fluh.git git checkout <某个适配分支> export PATH=$PWD/flutter_fluh/bin:$PATH flutter doctor

这里提醒一下,flutter doctor 输出里 OpenHarmony 相关的检查项可能不是绿色的,因为它的检测逻辑主要面向 Android,我们只要确认 flutter 命令能正常执行,版本号对得上,就可以继续。

创建工程时,有个小坑:早期版本的 flutter_fluh 默认不会生成 ohos 平台目录,需要手动执行flutter create --platforms=ohos .之类的命令,或者从模板里复制。不同分支能力不一样,我的做法是先建一个空工程跑通,再往里面加代码。

2.2 DevEco Studio 联动与签名配置

Flutter 工程创建完之后,真正的 OpenHarmony 侧工程结构是由 DevEco Studio 来处理的。这里要理解 Flutter 和 OpenHarmony 的分工:

  • Flutter 负责 Dart 侧的 UI 和业务逻辑
  • DevEco Studio 负责编译 OpenHarmony 端的壳工程(entry 模块)

我遇到的最影响效率的问题就是签名配置。OpenHarmony 应用在真机上运行必须配置调试签名,否则安装后会闪退或者被拒。DevEco Studio 需要登录并自动生成签名,同时还要保证签名文件对应的 bundle 名和 Flutter 工程里配置的包名一致。

建议把包名在工程初期就定死。我在改过一次包名之后,才体会到这个选择有多么重要:包名不统一,会导致 DevEco 侧签名的 bundleName 与 Flutter 侧生成的 profile 对不上,每次构建都要手动去调整,很消耗耐心。

2.3 热重载与调试体验差异

OpenHarmony 上的 Flutter 热重载体验比 Android 差一些,这是客观事实。热重载只对 Dart 代码生效,如果你修改了 ohos 目录下的原生代码,必须重新构建。还有一个特点:OpenHarmony 上热重载偶尔会失去响应,尤其是改动了涉及平台通道的部分。

我整理一个比较实用的操作习惯:把构建分成两步,先命令行编译,再用 DevEco 连接设备调试。

flutter build hap --debug

这条命令会把整个 HAP 包构建出来,然后通过 DevEco 或 hdc 安装到设备上。

如果你在 DevEco 里直接点运行,它会执行整链路编译,首次构建大概需要几分钟,后面增量会快很多。我的建议是日常写业务代码时,依赖 flutter run 连模拟器即可,确认逻辑没问题了再走 DevEco 打包。

3. 数据层设计:加油记录、存储选型与油耗计算逻辑

3.1 表结构设计:先定义一次加油记录

油耗追踪器的核心数据是加油记录。一个字段都不能少,我经过几版调整后,最终的表结构是这样:

字段类型说明
idTEXT主键,使用 uuid 生成
dateINTEGER加油时间戳,毫秒级
odometerREAL当前里程表读数(公里)
litersREAL本次加油升数
costREAL本次总花费(元)
price_per_literREAL单价,由 cost / liters 算出
is_fullINTEGER是否加满,0 或 1
noteTEXT备注,比如加油站名

is_full 这个字段对油耗计算至关重要。只看单次记录无法判断这箱油到底跑了多少,必须结合上次"满箱"记录来推算。所以我把是否加满单独拎出来,而不是让用户每次手动算。

3.2 本地存储:sqflite 还是 drift?

在 OpenHarmony 上做本地存储,比在 Android 上多了一层插件适配的顾虑。Android 生态里有 Room、GreenDAO 这些成熟方案,但在 Flutter for OpenHarmony 的生态里,我们只能从 Flutter 插件里找已经有 ohos 适配的。

我对比了两个方案:

  • sqflite:最成熟,有 OpenHarmony 适配版本(sqflite_ohos 或社区 fork),API 简单,SQL 字符串直接写,适合到处查数据的场景。
  • drift:基于 SQLite 的 ORM,类型安全,代码生成,但编译期依赖比较多,在 OpenHarmony 上集成需要额外验证。

最终选了 sqflite。原因很务实:油耗查询的 SQL 没几条,都是简单的聚合查询,不需要 ORM 的复杂映射来提升开发效率。

建表 SQL 是这样的:

CREATE TABLE fillups ( id TEXT PRIMARY KEY, date INTEGER NOT NULL, odometer REAL NOT NULL, liters REAL NOT NULL, cost REAL NOT NULL, price_per_liter REAL, is_full INTEGER DEFAULT 0, note TEXT );

另一个存储点是偏好设置,比如默认币种、油耗单位(L/100km 还是 km/L),这个用 shared_preferences 的 ohos 适配版就够了,不需要建表。

3.3 油耗计算的核心算法

油耗计算是整个 App 的灵魂,逻辑必须讲清楚。

单次油耗的正确算法是:用两次连续加满记录之间的里程差和加油量来计算。

举个例子:

  • 3月1日,里程 12000,加满 35L
  • 3月15日,里程 12480,加满 32L

从 3月1日到 3月15日,车跑了 480 公里,消耗了整整一箱油(后一次加满的量就是这段路程的消耗量),所以油耗是:

油耗(L/100km) = 32 / (12480 - 12000) * 100 = 6.67

注意:不能用第一次加油的升数,也不能抛开两次记录的先后顺序只看单次数据。这里有一个新手很容易犯的错误:把每次记录的 liters 字段直接当油耗计算分母。如果油箱没满,liters 只代表本次加了多少,不代表这段路耗了多少。

概览页需要展示几个聚合数据:

  • 平均油耗:最近 N 次有效油耗的加权平均数,权重就是里程差。
  • 总花费:SUM(cost),这个简单,但要注意按月份过滤时的时间戳边界。
  • 总里程:MAX(odometer) - MIN(odometer)。
  • 续航估算:油箱容量(设定值)除以最近一次油耗再乘 100。

这个计算逻辑我在 Dart 侧封装了一个类,每次查询数据库之后立刻生成统计结果,不缓存,因为数据量实在太小,一次全表扫描也就是几百条记录,性能完全不是瓶颈。

4. 概览页拆解:统计卡片的组件化实现

4.1 页面骨架:从"打开即用"到"无感刷新"

概览页是这个 App 的门面,设计原则就一条:打开就要让用户看到最重要的信息,不需要任何点击。

页面骨架用的是一列垂直布局,最上面是可滑动的统计卡片区,接下来是油耗趋势图,最下面是最近的加油记录列表。整体结构有点类似健康类 App 的首页——信息密度高,但不让人觉得乱。

根组件是 CustomScrollView,配合几个 Sliver,这样上下滑动时整个页面是一体的。我在页面创建时注册了数据库监听:

class OverviewPage extends StatefulWidget { @override State<OverviewPage> createState() => _OverviewPageState(); } class _OverviewPageState extends State<OverviewPage> { late Stream<void> _dataChangedStream; @override void initState() { super.initState(); _dataChangedStream = FillUpDatabase.instance.watchChanges(); } ... }

数据库发生变化后,通过 Stream 通知概览页刷新。这样即使你在别的 Tab 页面新增了一条加油记录,回到概览页时数据会自动更新,不用手动下拉刷新。

4.2 统计卡片组:一行四列还是两行两列?

统计卡片我想了很久,最后选择了两行两列布局。

一行四列在手机屏幕上太挤了,数字稍微大一点就溢出。两行两列有足够的空间展示大数字和单位。四个卡片分别是:

  1. 最近油耗:展示上一次有效计算的 L/100km 数值
  2. 平均油耗:一个阶段内的平均油耗
  3. 总花费:全部加油费用之和
  4. 总里程:所有记录的里程跨度

每个卡片是一个 StatelessWidget,接收数值和标题,只负责显示。卡片的实现很直观:

class StatCard extends StatelessWidget { final String title; final double value; final String unit; final Color accentColor; final VoidCallback? onTap; @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: ...), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(value.toStringAsFixed(1), style: ...), SizedBox(width: 4), Text(unit, style: ...), ], ), ], ), ), ), ); } }

卡片用 Material + InkWell 包了一层,这样点击时有涟漪效果。虽然现在只是跳到详情页,但后续如果加"点击卡片查看该指标的详细趋势",交互基础已经打好了。

4.3 数字动效:用 AnimatedSwitcher 做数据切换

概览页最影响观感的一个细节,是数据刷新时数字的切换。

如果不加动画,刷新时数字会直接跳到新值,显得生硬。我用了 Flutter 自带的 AnimatedSwitcher,在数值变化时做上下淡入淡出的效果,每次切换耗时 300 毫秒左右,不会让人觉得拖沓。

AnimatedSwitcher( duration: Duration(milliseconds: 300), transitionBuilder: (child, animation) { return FadeTransition( opacity: animation, child: SlideTransition( position: Tween<Offset>( begin: Offset(0, 0.2), end: Offset.zero, ).animate(animation), child: child, ), ); }, child: Text( value.toStringAsFixed(1), key: ValueKey(value.toStringAsFixed(1)), style: ..., ), )

这里有个关键细节:AnimatedSwitcher 是通过 child 的 key 来判断"值是否变化"的,所以必须给 Text 一个和数值强相关的 Key,否则相同的数字不会触发动画。我用的是格式化后的字符串作为 ValueKey,比如 "6.7"。如果两次刷新后的数值恰好一样,不触发动画,这个行为是合理的。

另外一个容易忽略的问题:文本宽度变化会导致布局抖动。数字从 6.7 变成 68.3 时,文本宽度变大了,卡片里的 Row 会重新布局。解决办法是给数字部分加Fixed宽度约束,或者使用 tabular figures(等宽数字)字体。我用了后者,在 TextStyle 里设置fontFeatures: [FontFeature.tabularFigures()],这个对数字类 UI 特别实用。

5. 油耗趋势图:用 CustomPainter 手写折线图

5.1 图表插件选型:老实的逻辑

概览页最核心的可视化部分,是油耗趋势图。

一开始我当然想到了 fl_chart,它是 Flutter 生态里最常用的图表库。但在 OpenHarmony 上,我犹豫了。fl_chart 本身是纯 Dart 实现,理论上不依赖平台能力,应该能跑。但问题在于它依赖的字体度量、文本缩放等能力,在某些 OpenHarmony 版本的 Flutter 引擎上表现不一致,而且图表库的事件处理(缩放、拖拽)在触摸屏驱动的适配上有时候会出现手势响应延迟。

我最后决定自己写一个折线图。理由有三:

  1. 油耗数据只有几十个点,不需要性能优化
  2. 折线图加渐变填充的绘制逻辑不复杂,100 行左右的 CustomPainter 就能搞定
  3. 自己绘制的可定制性强,后续配色、标签、图例想改就改

这个选择可能不符合"快速开发"的思路,但从技术验证的角度看,在一个不算成熟的平台上,减少第三方依赖永远是对的。

5.2 折线图绘制流程拆解

绘制流程分成三步:坐标映射、折线路径、渐变填充。

坐标映射的关键是 y 轴范围。不能直接取数据的最小值和最大值,因为如果有一箱油特别费油(比如 12L/100km),而大部分数据都在 6-8 之间,整个曲线就会变得很平,看不出变化趋势。我给 y 轴加了 20% 的上下留白:

final minY = data.reduce(min) * 0.8; final maxY = data.reduce(max) * 1.2;

x 轴是等距的,因为加油记录不一定每天都有,按照时间戳算间距会让点分布不均匀,用索引号等距分布更美观。

绘制折线的核心代码:

class FuelTrendPainter extends CustomPainter { final List<double> values; final Color lineColor; final Color fillColor; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2.5 ..strokeCap = StrokeCap.round ..color = lineColor; final path = Path(); final n = values.length; final dx = size.width / (n - 1); for (var i = 0; i < n; i++) { final x = i * dx; final y = normalizeY(values[i], size.height); if (i == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } canvas.drawPath(path, paint); // 填充 final fillPath = Path.from(path) ..lineTo(size.width, size.height) ..lineTo(0, size.height) ..close(); canvas.drawPath(fillPath, Paint()..style = PaintingStyle.fill..color = fillColor); } }

有个小细节容易踩坑:当n == 1时,dx会变成无穷大,size.width / (n - 1)直接除零崩溃。我在实际开发中先做了数据过滤,只有两条及以上的记录才显示趋势图,否则显示一个"记录太少,继续加油吧"的占位图。

5.3 双维度对应:油耗还是费用曲线?

趋势图只有一个曲线不太过瘾,我把它做成了"双数据维度":默认显示油耗曲线,右上角有一个小的切换按钮,可以切换成费用曲线。

费用曲线同样用 CustomPainter,只是把数据源从"每百公里油耗"换成了"每次加油的总费用"。这样用户既能看出油耗稳定性,也能看出消费趋势。

切换时我用了一个 TweenAnimationBuilder,让两条曲线之间有平滑的过渡效果:

TweenAnimationBuilder<double>( tween: Tween(begin: 0, end: 1), duration: Duration(milliseconds: 400), builder: (context, t, child) { return CustomPaint( painter: FuelTrendPainter( values: isFuelMode ? fuelValues : costValues, progress: t, ... ), ); }, )

这个过渡不是自动的,要配合一个动画控制器在isFuelMode变化时重置 Tween,视觉上就是从一条曲线渐变到另一条曲线。

6. EventChannel 与原生能力打通:油量、电量与通知

6.1 为什么要用 EventChannel

FillUp 虽然核心业务是记录,但我希望概览页还能展示一些来自系统侧的实时数据,比如当前设备的油量信息。如果有蓝牙 OBD 设备连接,原生的 CAN 总线数据就可以通过这个通道传给 Flutter。

这就涉及 Flutter 平台通道里的 EventChannel。它和 MethodChannel 的区别在于:

  • MethodChannel 是一次性请求-响应模式
  • EventChannel 是持续的事件流,原生侧主动往 Flutter 推数据

在 OpenHarmony 上,EventChannel 的 Flutter 侧 API 和标准 Flutter 完全一致,差异在原生侧实现。

Flutter 侧代码:

static const EventChannel _fuelLevelChannel = EventChannel('fillup/telemetry/fuel_level'); Stream<double> get fuelLevelStream { return _fuelLevelChannel.receiveBroadcastStream().map((event) { return (event as num).toDouble(); }); }

6.2 ohos 侧原生实现

OpenHarmony 侧的实现基于 DevEco Studio 的 ArkTS 代码。需要在 entry 模块的 MainAbility 里注册 EventChannel,并在模块侧通过EventChannel的setStreamListener提供数据源。

核心逻辑是这么几步:

  1. 初始化 EventChannel,名称和 Flutter 侧保持一致
  2. 通过setStreamListener监听消费者是否订阅
  3. 订阅后,通过后台任务周期性从系统接口读取油量数据,调用eventSink?.success(data)推送
let eventChannel = new rpc.EventChannel('fillup/telemetry/fuel_level'); eventChannel.setStreamListener({ onEvent: (eventSink) => { this.sink = eventSink; this.startTelemetry(); }, onCancel: () => { this.stopTelemetry(); } });

这里面有一个特别需要注意的问题:EventChannel 的推送频率不能太高。如果每 100 毫秒推一次,Flutter 侧会因为事件积压出现 UI 卡顿,尤其是在 ListView 滑动时。

我最后把推送频率限制在 1 秒一次,并且只在车牌识别到车辆启动后才开始推送,避免电量消耗。

6.3 PlatformView 场景预留

虽然 FillUp 现在没有内嵌原生地图,但我在工程规划里预留了 PlatformView 的集成位置。如果后续版本要显示"常用加油站在哪里"的导航入口,可能要用原生地图组件。

在 OpenHarmony 的 Flutter 适配中,PlatformView 是一个比较特殊的组件:Flutter 侧用UiAwareView或PlatformViewLink来承载原生视图,终端侧需要实现PlatformView接口,然后返回原生组件。这个流程和 Android 的 PlatformView 类似,但 API 有差异。

我在这块的策略是:先把接口定义好,不去实现,等真需要的时候再填坑。

7. 踩坑实录:TabBar 动画、组件通信与页面状态保持

7.1 IndexedStack 保命:切换 Tab 不丢状态

FillUp 的主页面结构是底部三个 Tab:概览、记录、设置。一开始我用的是NavigationBar加IndexedStack的方式。

IndexedStack的好处是:三个页面的状态同时保留,切换时不会重新创建。这个对概览页特别重要,因为概览页包含一个滚动位置和一个已绘制好的趋势图,如果每次切换 Tab 都销毁重建,用户会明显感觉到卡顿和数据重置。

后来我测试了另一种方案:用PageView加AutomaticKeepAliveClientMixin。这个方案的问题是,页面不在当前屏时仍然会被系统回收,只是通过 KeepAlive 机制保留状态,行为在 OpenHarmony 上表现得不太稳定。最终我还是回到IndexedStack。

为什么 IndexedStack 在 OpenHarmony 上表现更好?因为它把三个子页面一次性全部加入树中,只是通过 opacity 控制显隐,不涉及页面生命周期调度。看起来三个页面同时存在会浪费内存,但对 FillUp 这种轻量页面来说,总页面数量固定且不多,这点开销完全值得。

7.2 TabBar 点击取消动画

底部 Tab 切换时,Flutter 默认的 NavigationBar 会有一个水波纹加指示器滑动的动画,大概 300 毫秒。

在 OpenHarmony 上,这个动画有时会导致快速连续点击时页面响应不同步,尤其是点击 Tab 后马上又想操作概览页的手势,动画还没结束,手势就被吃掉了。

解决办法有几种:

  • 用NavigationBar的labelBehavior调整显示方式,但动画没法直接取消
  • 换成自绘底部导航栏,用GestureDetector手动控制页面切换
  • 保持NavigationBar不变,在快速点击时忽略多余的事件

我最后选择了自绘底部导航栏,因为这样可以直接控制点击行为,不加动画。自定义导航栏的核心逻辑是:

Row( children: List.generate(_tabs.length, (index) { return Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { setState(() { _currentIndex = index; }); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(_tabs[index].icon), Text(_tabs[index].label), ], ), ), ); }), )

没有InkWell的涟漪,没有淡入淡出,页面切换就是 instant 的。从交互角度说,这种硬切换更适合工具类 App,用户不会在意滑动指示器有多丝滑,只在意响应快不快。

7.3 组件通信:InheritedWidget 到 Stream

填写加油记录的页面在第二个 Tab,概览在第一个 Tab,两个页面之间怎么通信,这个问题虽然不大,但也能看出一个人的工程习惯。

我的方案是组合拳:

  • 数据库变化通过StreamController.broadcast()广播,概览页监听后刷新
  • 设置页修改了单位(L/100km 还是 km/L),通过共享的 AppState 通知概览页重新格式化数字
  • 组件级的局部状态(比如某个卡片是否展开)用ValueNotifier,没必要全局管理

最不推荐的做法是每个小状态都走全局状态管理框架。FillUp 这种项目规模,Riverpod 或 Bloc 的重度使用只会增加代码复杂度,不会带来实质收益。我在记录页和概览页之间共享的数据只有两类:加油记录列表和设置项,用一个ChangeNotifier就能覆盖。

热词里有人提到 cubit,我理解对于一些大型项目,Bloc/Cubit 是有它的优势的,尤其是状态流转复杂、需要严格分离业务逻辑和 UI 的时候。但如果你只是为了在三个 Tab 页面之间传一个油耗数值,引入 cubit 属于用大炮打蚊子。

8. 性能调优:Impeller 渲染与页面切换体验

8.1 Impeller 在 OpenHarmony 上的表现

Flutter 3.x 引入了 Impeller 渲染引擎,在 iOS 上默认开启,在 Android 和 OpenHarmony 上还在逐步推进。flutter_fluh 的某个版本分支开始支持 Impeller 的编译开关。

在 OpenHarmony 上,Impeller 的收益和 Android 类似:减少首帧包体积,提升渲染一致性。

不过我在实测中发现,FillUp 这个项目的页面复杂度并不高,Impeller 和 Skia 的表现差异基本感知不到。真正影响体验的是列表页滚动时的像素重绘,不是渲染引擎。

如果你的应用有大量阴影、圆角、渐变,那 Impeller 在 OpenHarmony 上应该能发挥优势,因为它的渲染策略对 GPU 更友好。但如果只是简单的卡片布局,不需要为 Impeller 纠结,用默认引擎就够了。

8.2 滑动性能:从卡顿到流畅的关键优化

概览页有一个叠加在 CustomScrollView 上的趋势图,滑动时 CustomPainter 会反复触发重绘,最开始这个区域在滚动时明显掉帧。

排查下来发现原因不是绘制本身,而是火焰图里显示的文本布局耗时。趋势图下面的标签(日期、油耗数值)每帧都在重新布局,而文本布局在 Flutter 里是比较重的操作。

优化手段是:

  • 把静态标签用RepaintBoundary包起来,让 Flutter 知道这部分不随滚动变化
  • 趋势图区域的 CustomPaint 设置isComplex为 true,提示引擎缓存绘制结果
  • 给数值文本用TextPainter提前缓存一次布局结果,而不是每帧重新 layout

经过这三项优化,概览页在 OpenHarmony 模拟器和真机上都能稳定跑到 60fps。

RepaintBoundary( child: CustomPaint( painter: FuelTrendPainter(...), isComplex: true, willChange: false, ), )

willChange这个参数很多人会忽略。如果一帧内绘制结果稳定不变,设成 false 可以启用光栅化缓存。但要注意,如果你的图表会动态更新(比如动画),必须设成 true,否则会出现显示不更新的问题。

8.3 后续扩展方向

FillUp 的概览实现到这一步已经覆盖了核心需求。后续可以扩展的方向我简单列一下:

  • 月度汇总对比:把按月的油耗和费用做成柱状图,判断季节对油耗的影响
  • CSV 导出:把数据库内容导出成表格文件,用于备份或进一步分析
  • 故障码读取:通过蓝牙 OBD 模块读取车辆故障码,在概览页显示整车健康状况
  • 多车管理:为家里另一辆车建立独立的数据空间,表结构里增加 vehicle_id 字段

如果真要支持多车,数据模型调整要趁早,否则后续迁移数据很麻烦。这是我在 FillUp 开发到一半时想到的,幸好当时表结构里预留了足够弹性,加一个外键字段就能支持。

写到这里,FillUp 的概览实现也告一段落了。作为验证 Flutter 在 OpenHarmony 上开发体验的项目,它的价值已经达到了。一个工具类应用从数据模型、存储、UI 到平台通道,全链路跑通,开发过程中踩过的坑也都找到了对应的解法。接下来我打算把这次的经验整理成一份适配清单,方便社区里的其他开发者少走弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询