1. 项目概述
1.1 能做什么:维修车间里的“数字仪表盘”
先说结论:这个项目,是给汽车维修门店/售后车间做的一个跨端“状态总览屏”。它解决的核心问题,是让前台接待、车间主管、维修技师都能在一块屏幕上随时掌握每辆在修车辆的实时状态——待接车、检测中、维修中、待付款、已完工,五个状态一目了然。不用再扯着嗓子问“那台白色的车修好了没”,也不用拿着对讲机反复确认工位进度。
这个界面本身不复杂,但有意思的地方在于它的实现方式:Flutter 作为跨端 UI 框架,跑在 OpenHarmony 系统上。也就是说,同一套 Flutter 代码,既能编出 Android/iOS 版本,也能跑在国产 OpenHarmony 设备上。对于维修连锁门店来说,这意味着前台那台鸿蒙触控平板、车间里的鸿蒙电视盒子、甚至后台管理员的手机,都能用同一套代码覆盖,不需要每个平台单独招人维护。
适合谁来参考?三类人:
- 做跨端应用、正在调研 Flutter 在 OpenHarmony 上可行性的开发者;
- 做汽修/售后类 SaaS 系统、需要快速落地终端界面的产品和技术;
- 对“Flutter 多端适配”感兴趣,想看看实际踩坑过程的同学。
我个人是在一个模拟项目X里完整趟了一遍这个流程,从环境搭建到真机调试再到性能调优,踩了不少坑。这篇文章把整个实现过程拆开讲,包括我为什么选 Flutter、OpenHarmony 那边要做哪些原生侧准备工作、状态数据怎么管理、卡片化 UI 怎么做,以及真机调试时需要特别注意的问题。
1.2 核心需求解析
在动手写代码之前,先花点时间把需求拆干净。这个界面看上去只是一个“状态卡片列表”,但背后其实有三个层次的问题:UI 层怎么展示、数据层怎么同步、以及跨端能力怎么打通。
| 层次 | 要解决的本质问题 | 我最终采用的方案 |
|---|---|---|
| UI 层 | 五种维修状态如何清晰区分,同时保持界面整洁 | 卡片式列表 + 状态色块 + 时间倒序排列 |
| 数据层 | 维修状态从哪儿来、多久刷新一次 | 门店本地服务接口 + 定时轮询 + 手动下拉刷新 |
| 跨端层 | Flutter 代码如何在 OpenHarmony 上正常运行 | flutter_flutter 适配层 + OpenHarmony SDK 桥接 |
UI 层相对比较简单,但数据层和跨端层才是真正的硬骨头。车辆维修状态不是静态数据,它是随时在变的——技师点击“开始维修”按钮、质检员提交“检测完成”、前台标记“已付款”,每一条记录都会改变状态。因此,界面必须做到实时性足够好,同时不能因为频繁刷新导致性能折损。
跨端层则是这个项目的核心难点:OpenHarmony 不是 Android,它的 API 接口、UI 线程模型、包管理机制都和 Android 有差异。Flutter 官方对 OpenHarmony 的适配工作虽然已经比较成熟,但具体到某个功能点时,仍然有不少细节需要自己处理。
2. 方案选型:为什么走 Flutter × OpenHarmony
2.1 跨端框架选型背后的取舍
先说说我为什么选 Flutter,而不是 React Native、uni-app 或者其他方案。其实原因很简单:维修门店的终端设备形态太杂了。
我有一次去某个维修连锁门店调研,发现他们的设备是这样的:前台有一台安卓触屏一体机,车间墙上挂着一台鸿蒙系统的电视盒子接大屏,店长用的手机是 iPhone,还有几个老员工用的安卓千元机。如果每个平台都要原生开发一套,光是维护成本就能压垮一个小团队。跨端框架几乎是必然的选择。
那么为什么在 Flutter、React Native、uni-app 之间选了 Flutter?三个原因:
UI 一致性强。React Native 和 uni-app 的底层渲染依赖各平台的原生控件,不同设备的显示效果总会有细微差异。而 Flutter 是自绘引擎,GPU 直接渲染,在安卓、iOS、OpenHarmony 上都能做到像素级一致。维修状态卡片上的色块、圆角、阴影,不会因为设备不同而“看起来不太一样”。
性能表现更适合信息密集场景。维修状态概览界面虽然看起来只是几张卡片,但真实场景中可能要同时展示 20-30 辆车的状态,每辆车有车牌号、车型、维修项目、工位号、剩余时间等字段。Flutter 的虚拟化列表机制(ListView.builder)在这种长列表场景下性能表现很稳。
OpenHarmony 适配进展快。华为官方和社区一直在推进 Flutter 对 OpenHarmony 的适配,目前大多数核心功能已经可用。相比其他跨端框架,Flutter 在 OpenHarmony 上的成熟度是相对较高的。
2.2 那些被我否决的方案
关于“为什么不直接用原生开发 OpenHarmony 应用”,很多人会问这个问题。答案很简单:成本和生态。OpenHarmony 的 ArkTS(方舟开发语言)更像 TypeScript 的变体,语法风格上能在短时间内上手,但其第三方生态远不如 Flutter 丰富。维修门店需求随时可能变——比如下周要加一个“维修进度拍照上传”功能,或者要对接微信小程序扫码查看维修进度。这种需求用 Flutter 实现,因为有庞大的插件生态支撑,开发速度会快很多。
但我必须说明一个事实:Flutter 在 OpenHarmony 上的适配并不完美。某些能力(比如蓝牙打印、串口通信,维修车间里常有这种需求)OpenHarmony 的 Flutter 插件可能还没有覆盖。越底层的硬件能力越要谨慎依赖跨端适配,真要踩到这个坑,再切回原生代码就麻烦了。
所以最终的架构选择是:UI 全部用 Flutter 写,但业务能力(如读设备信息、调用系统能力)通过 MethodChannel 桥接到 OpenHarmony 原生侧。这种“Flutter 外壳 + 原生内核”的思路,既保证了 UI 开发效率,又给底层能力留了后门。
2.3 OpenHarmony 侧适配现状说明
先给还没接触过 OpenHarmony 的人做个背景交代:OpenHarmony 是开源的操作系统,由开放原子开源基金会孵化。它和 HarmonyOS 不完全一样——HarmonyOS 是商业发行版,OpenHarmony 是底层开源项目。做开发时,我们通常把 OpenHarmony SDK 当作一个“类 Android 但不是 Android” 的平台来对待。
Flutter 跑在 OpenHarmony 上,走的是社区适配分支(flutter_flutter 的 OpenHarmony 分支)。从实际体验看,截至近期,基础渲染、文本输入、网络请求这些核心功能都很稳定,真机跑起来没遇到闪退。但要注意:这不是官方的稳定发布分支,而是社区维护的 fork,版本更新滞后于 Flutter 官方主线。因此在实际项目中,建议锁定某一个经过验证的特定版本,不要盲目追新。
我搭建项目时用的是 Flutter 3.x 的一个稳定版本适配 OpenHarmony 的分支,具体次要版本号这里不细说,因为你如果直接搜索“flutter ohos”,找到的适配分支版本可能已经变了。给一个实用建议:去 OpenHarmony 官方文档或社区找“flutter 适配”相关页面,看他们当前推荐的版本组合,照抄就行。
3. 环境搭建与工程结构
3.1 需要准备的工具链
这个项目涉及两套工具链:Flutter 开发链和 OpenHarmony 开发链。
- Flutter SDK(建议不低于 3.x 版本)
- OpenHarmony SDK(建议 4.x 及以上,包含 ArkTS 编译工具链)
- DevEco Studio(OpenHarmony 应用开发的 IDE,类似于 Android Studio 的角色)
- 一台 OpenHarmony 真机或模拟器(真机调试最稳)
- Flutter 的 OpenHarmony 适配分支代码
我们团队的实际配置是:用一台安装了 DevEco Studio 的 Windows 工作站做 OpenHarmony 侧开发,用另一台 Mac 写 Flutter 代码。Flutter 代码写完,同步到 Windows 工作站上,通过 DevEco Studio 工程里的 flutter 模块集成编译,最终打包出可安装到 OpenHarmony 设备上的 hap 文件(类似 Android 的 apk)。
这个跨设备协作的方式,如果你是一个人开发,其实也可以把两套工具链装在同一台电脑上,只是要注意环境变量冲突。
3.2 搭建 Flutter 工程并集成 OpenHarmony 平台
集成步骤拆开来看是这样的:
- 在 Windows 工作站上安装 DevEco Studio,并且安装好 OpenHarmony SDK。
- 用 DevEco Studio 新建一个空的 OpenHarmony 工程(选 Empty Ability 模板即可)。
- 把新建的工程作为“宿主工程”,然后把 Flutter 需要的 ohos 目录(由 flutter_flutter 适配分支生成模板)放到工程目录里。
- 在宿主工程中通过 module 方式引入 Flutter 模块。
- 在工程的 module.json 文件里配置 Flutter 模块,注册相关的 Activity 入口。
- 用 Flutter 写 UI,用 DevEco Studio 编译整个宿主工程,生成 hap 包。
注意 один момент:OpenHarmony 工程的构建系统是
hvigor,它和 Android 的 Gradle 完全不同。初次接触时最大的坑是你用 Flutter 的方式去思考构建流程——不对,hvigor 有自己的配置方式和缓存机制,遇到构建失败时先查它的日志,而不是一头扎进 Gradle 的思维模式里。
3.3 工程目录的关键模块
最终搭建好的工程目录里,真正需要自己动手改内容的模块主要包括:
entry:OpenHarmony 应用的入口模块,负责配置应用权限、声明能力(Ability)、打包入口。ohos/flutter_module:Flutter 侧代码的宿主,Flutter 的页面在这里注册。lib:Flutter 的 dart 源码目录,UI 和业务逻辑主要在这里。entry/src/main/ets/pages:OpenHarmony 原生页面入口,负责加载 Flutter 模块。
这个结构要理解清楚:OpenHarmony 那个工程是“宿主”,Flutter 是“客房”。客房可以装修得再漂亮,但入住得通过宿主的门禁系统——也就是说,OpenHarmony 原生侧的应用入口和生命周期管理是老大, Flutter 页面只是被“加载”进来的一个模块。
3.4 第一次构建的崩溃现场
说实话,第一次构建这个工程我是崩溃的。从 pull 代码到最终跑出 hello world,花了整整一个下午。最大的问题出在版本匹配:Flutter 适配分支要求的 OpenHarmony SDK 版本和我本地 DevEco Studio 带的 SDK 版本不一致,结果就是编译报出一堆莫名其妙的格式错误。
后来学乖了,先查适配分支的 README 和 release note,确认支持哪些 OpenHarmony SDK 版本,再用对应版本的 DevEco Studio 建空工程。这个环节不要有侥幸心理,严格按官方映射匹配版本,一套一套地配齐。
另外,建议第一次跑通 hello world 之后立刻做一次“备份”——把整个 DevEco Studio 配置、SDK 路径、环境变量记录成文档。这个项目我中途重装了开发机,恢复环境时全靠这份文档,省了不止三个小时。
4. 车辆维修状态数据的建模与管理
4.1 状态字段设计
维修状态概览界面的核心是数据模型。我设计的数据结构是这样的:
class RepairStatus { final String id; // 维修单号 final String plateNumber; // 车牌号 final String model; // 车型 final String repairType; // 维修类型:保养/维修/钣喷 final String technician; // 负责技师 final String bayNumber; // 工位号 final RepairPhase phase; // 核心:维修状态 final DateTime updatedAt; // 状态更新时间 final int progressPercent; // 进度百分比,仅维修/检测状态使用 final String? note; // 备注,比如“等配件中” }这里的核心字段是phase,它是一个枚举类型:
enum RepairPhase { pending, // 已接车待安排 inspecting, // 检测中 repairing, // 维修中 awaitingPayment, // 待付款 completed // 已完工 }五种状态对应五种不同的视觉表现。在真实业务中,状态往往不只有这五种,比如可能有“待配件”(零部件缺货导致停工)、“待质检”等子状态。但从视觉呈现角度,子状态过多会让界面变得混乱。我的做法是:主状态保持五种,子状态放进note字段里用文字表达——比如“维修中 - 等配件,预计明天到”。
4.2 状态流转逻辑与实时更新机制
维修状态的流转不是随意的,它有严格的权限控制。比如,技师不能直接把车从“检测中”改成“已完成”,必须先进入“维修中”再进入“待付款”。这个业务规则如果放在 UI 侧自由修改,会出乱子。
因此,我的设计中,Flutter 端只负责展示状态,状态变更通过接口由后端统一控制。Flutter 端通过 HTTP 接口拉取状态列表,页面数据源就是一个简单的List<RepairStatus>。
实时更新采用轮询机制:Timer.periodic每 15 秒请求一次接口,同时支持下拉刷新和右上角手动刷新按钮。为什么不用 WebSocket 或者服务端推送?一是因为维修车间网络环境有时不稳定(尤其是工位附近有大型设备干扰信号),长连接容易断;二是轮询的实现足够简单,15 秒对于维修状态这种低频变化场景来说,体验上感知不到延迟。
提示:轮询时注意处理页面退后台的场景。我在开发初期忽略了这个,结果 App 切到后台后 Timer 还在跑,耗电明显。后来接入了
AppLifecycleListener,在应用进入非活跃状态时暂停轮询,回到前台再恢复。
4.3 接口返回的容错与兜底
真实门店的接口不可能永远 200 OK。维修系统是老系统改过来的,偶尔会返回 500、超时、甚至返回的数据里某些字段为空。
对这种情况,我在模型层做了一个fromJson的硬解码,并且在解析失败时返回一个“不详”占位值,而不是直接抛异常让整个列表白屏。
factory RepairStatus.fromJson(Map<String, dynamic> json) { return RepairStatus( id: json['id']?.toString() ?? '', plateNumber: json['plate']?.toString() ?? '未知', model: json['model']?.toString() ?? '未知车型', repairType: json['type']?.toString() ?? '维修', technician: json['tech']?.toString() ?? '待分配', bayNumber: json['bay']?.toString() ?? '未分配', phase: _parsePhase(json['phase']?.toString() ?? ''), updatedAt: DateTime.tryParse(json['updatedAt']?.toString() ?? '') ?? DateTime.now(), progressPercent: int.tryParse(json['progress']?.toString() ?? '0') ?? 0, note: json['note']?.toString(), ); } static RepairPhase _parsePhase(String raw) { switch (raw) { case '0': return RepairPhase.pending; case '1': return RepairPhase.inspecting; case '2': return RepairPhase.repairing; case '3': return RepairPhase.awaitingPayment; case '4': return RepairPhase.completed; default: // 后端可能传英文、中文、数字等不同格式 if (raw.contains('待') || raw.contains('pending')) return RepairPhase.pending; if (raw.contains('检测')) return RepairPhase.inspecting; if (raw.contains('维修')) return RepairPhase.repairing; if (raw.contains('付款') || raw.contains('结算')) return RepairPhase.awaitingPayment; return RepairPhase.completed; } }这个“柔性解析”的策略看起来比较蠢,但真的处理了很多实际现场问题。老系统的后端字段很随意,什么格式都有可能传出来。
4.4 维修进度百分比的计算逻辑
进度百分比不是实时从设备端采集的,维修过程没有传感器能自动换算成百分比。实际做法是:由维修系统根据当前工序计算。比如维修流程分为四个大工序:接车检查、拆解维修、装复试车、清洁交车,每个工序默认 25%。如果技师在某个工序中标记了“已完成”,后端自动累加。
前端不做任何进度计算,接口返回多少就展示多少。这里有个 UI 层面的小细节:当progressPercent等于 100 时,卡片上的进度条要自动隐藏,换成“待付款”状态色块——否则进度条满格了但状态还是“维修中”,会让前台误判。
5. 卡片式 UI 的构建与视觉设计
5.1 整体布局框架
维修状态概览界面的布局,本质上是一个“可筛选的列表”。
Scaffold( appBar: AppBar( title: Text('车辆维修状态'), actions: [ IconButton( icon: Icon(Icons.refresh), onPressed: _refreshData, ), ], ), body: Column( children: [ _buildPhaseFilter(), // 顶部筛选栏 Expanded( child: RefreshIndicator( onRefresh: _refreshData, child: ListView.builder( itemCount: _statusList.length, itemBuilder: (context, index) => RepairCard( status: _statusList[index], ), ), ), ), ], ), )顶部筛选栏允许按维修状态过滤,默认显示全部。因为维修车间的数量可能很多(连锁门店高峰时可能有几十台车在修),筛选功能在实际使用中是高频操作——前台经常只关注“完工待交车”的车。
5.2 卡片视觉设计的细节逻辑
卡片是信息承载体,设计上我的原则是:一眼扫过去就知道哪辆车最紧急。
五种状态对应的主色:
待接车安排: 灰色(Colors.blueGrey)检测中: 蓝色(Colors.blue)维修中: 橙色(Colors.orange)待付款: 红色(Colors.red)已完工: 绿色(Colors.green)
颜色选择上,红色系的“待付款”用的是高饱和度的 red,因为它是门店营收的最后一环,前台需要第一时间注意到。而“已完工”用绿色——交车是关键节点。
卡片本身用 Card+InkWell 组合,点击进入维修详情页(这个页面我还没做,只留了占位回调)。每张卡片的关键信息排布:
- 顶部:车牌号(大号字体)+ 车型(小号灰色)
- 中间:维修类型 + 工位号 + 负责技师
- 底部:更新时间 + 进度条 或 备注
这里有一个操作层面的建议:车牌号不要用太炫的字体。维修车间环境光线复杂,屏幕又常是横放或远距离观看,清晰度才是第一位的。
5.3 卡片内进度条的自定义实现
Flutter 自带的LinearProgressIndicator可以满足基本需求,但它只显示一个进度条,不能显示百分比。我在维修中状态下,额外在进度条右侧加了百分比文字:
Widget _buildProgressIndicator(BuildContext context, RepairStatus status) { return Row( children: [ Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: status.progressPercent / 100, minHeight: 6, backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation<Color>(_phaseColor(status.phase)), ), ), ), SizedBox(width: 8), Text( '${status.progressPercent}%', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ); }这里有个性能细节:LinearProgressIndicator如果传入的是固定值(非动画),它不会触发持续重绘,所以对列表性能影响很小。但如果误用了不确定进度模式(value: null),它会一直播放动画,列表滚动时会明显卡顿——不要给正在轮询刷新的列表再添这种持续动画。
5.4 列表性能:别用花哨效果堆死低端机
维修门店的设备不都是旗舰机。车间里的平板、电视盒子很多是低端配置。长列表场景下,性能优化有几个基本动作:
ListView.builder必须用 builder 构造,不能用ListView(children: [...])直接把所有卡片一次性创建。- 卡片组件做成
const构造(除状态数据外),减少重建成本。 - 避免大阴影(
Card的默认阴影在小内存设备上绘制开销不小,可以换成elevation: 2或干脆用带边框的容器模拟卡片效果)。 - 图片资源全部用 webp 格式,不用 png/jpg 大图。
我实测过一个很老的 OpenHarmony 盒子,直接用默认 Card 阴影的列表,翻页时会有掉帧;把阴影降级为边框后,流畅度明显提升。
5.5 状态切换的动画处理
状态变化时,卡片会有轻微的视觉反馈:透明度+向上位移的淡入动画。这是我用AnimatedSwitcher实现的,当卡片的状态字段变化时,整个卡片 fade+slide 切换。
AnimatedSwitcher( duration: Duration(milliseconds: 300), child: RepairCard( key: ValueKey('${status.id}_${status.phase}_${status.updatedAt}'), status: status, ), )这里的关键是 key:只有当 status 的 phase 或 updatedAt 变化时才创建新 key,触发动画;数据轮询返回相同数据时,key 不变,就不会频繁动画。否则每 15 秒刷新一次,所有卡片都在闪,前台能看花眼。
6. OpenHarmony 原生侧接桥与能力调用
6.1 MethodChannel 通信桥的实现思路
Flutter 在 OpenHarmony 上跑起来之后,有些能力 Flutter 侧是不直接提供的。我这边的场景是:需要获取设备的一些基本信息(比如设备型号、屏幕尺寸),用于界面的自适配。这些信息原生侧很容易读到。
MethodChannel 在 OpenHarmony 上的用法和 Android 上相似:
// Flutter 侧 MethodChannel _channel = MethodChannel('com.example.repair/device'); Future<Map<String, dynamic>> getDeviceInfo() async { try { final result = await _channel.invokeMethod('getDeviceInfo'); return Map<String, dynamic>.from(result); } on PlatformException catch (e) { // 兜底处理 return {'model': 'unknown', 'screenWidth': 0}; } on MissingPluginException catch (e) { // 通道未注册 return {'model': 'unknown', 'screenWidth': 0}; } }原生侧在 OpenHarmony 的 ArkTS 中注册对应的 Channel,需要先找到 Flutter 模块的入口 Ability(通常是FlutterActivity或FlutterFragment),在其生命周期方法中注册:
let messenger = this.flutterEngine?.getAppContext() // 或通过 flutter module 的注册接口获取 BinaryMessenger let channel = MethodChannel('com.example.repair/device', messenger) channel.setMethodCallHandler((call, result) => { if (call.method === 'getDeviceInfo') { // 读取设备信息 result.success({ model: deviceInfo.getModel(), screenWidth: display.getScreenWidth(), screenHeight: display.getScreenHeight() }) } })还有一个更省事的方式:不用 MethodChannel,而是通过 OpenHarmony 的 Flutter 适配层直接提供“自定义 PlatformView”。但 PlatformView 在 OpenHarmony 上还不太成熟,我试过一次,性能不行,就放弃了。
6.2 网络权限与其他权限声明
OpenHarmony 的权限声明机制和 Android 一脉相承,在module.json5中声明。
我要联网拉维修状态接口,所以必须声明ohos.permission.INTERNET:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }这个权限声明如果不加,编译没有报错,运行时网络请求会直接失败,错误信息不明显,真的很容易踩坑。
6.3 原生侧闪退与日志排查思路
OpenHarmony 应用如果原生侧闪退,你很难用 Flutter 的 Debug 模式捕捉到异常。因为 Flutter 是运行在独立线程里,原生闪退往往是 multithreading 问题,Flutter 侧完全感知不到。
排查思路是:先用 DevEco Studio 打开系统日志,过滤 hap 包名,查看崩溃栈。OpenHarmony 的设备侧日志在真机上比较复杂,需要通过hdc命令行工具获取底层日志(hdc 类似于 Android 的 adb)。
hdc shell hilog是主要的日志抓取命令。我在项目调试阶段,几乎每天都要用类似这样的组合:
hdc shell hilog | grep -E "your.package|flutter|FATAL"有一次崩溃是因为 Flutter 的纹理(Texture)实现在 OpenHarmony 上遇到一个未适配的 GPU 驱动,导致渲染线程崩溃。日志里的关键字是GLES,排查了半天,最后发现是刷新率设置问题——部分 OpenHarmony 设备默认 60Hz,但适配层的纹理同步机制在特定驱动组合下会触发断言。这种问题没有通用解法,看到类似报错时,先检查 Flutter 适配分支的 issue 列表,看看有没有人提到同款问题。
7. 数据接入与交互体验优化
7.1 从“能显示”到“用得顺”的关键优化
如果你的目标是做一个能用的 Demo,可能代码写完就结束了。但真实门店场景里,从“能显示”到“用得顺”之间隔着好几层优化。我在这个项目中最有感触的,是这些小细节。
下拉刷新必须有。前台看到的数据可能是 15 秒前的,但人心里不踏实,特别是客户来问车修好了没的时候,她会下意识地下拉刷新一下。 RefreshIndicator 加上,交互就很自然。
空状态的展示。门店午休时段,所有维修任务都没了,列表空荡荡的。如果只显示一个空白列表,前台会怀疑 App 坏了。需要一个“当前无维修车辆”的空状态插画——用 Flutter 的Icon+ 文字即可,不用额外引入图片库。
时间显示相对化。界面底部显示的更新时间,我做了“刚刚/X分钟前/X小时前”的转换。原因很简单:如果用绝对时间,前台在远处瞄一眼,还要在脑子里算一下 14:32 距离现在多久。相对时间一眼就懂。
String formatRelativeTime(DateTime time) { final diff = DateTime.now().difference(time); if (diff.inMinutes < 1) return '刚刚'; if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前'; if (diff.inHours < 24) return '${diff.inHours}小时前'; return '${diff.inDays}天前'; }设备横屏适配。车间里的大屏很多时候是横放的。OpenHarmony 设备的方向变化,Flutter 侧能通过MediaQuery感知到。维修状态卡片在竖屏和横屏下,我用了不同的布局逻辑:竖屏是单列卡片列表,横屏变成双列 GridView,视觉更紧凑。
final orientation = MediaQuery.of(context).orientation; if (orientation == Orientation.portrait) { return ListView.builder(...); } else { return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1.4, ), ... ); }7.2 弱网/无网状态下的兜底策略
维修车间的 Wi-Fi 质量大家心里有数——金属货架多、设备干扰严重,偶尔断网太正常了。Flutter 的网络请求如果失败就直接报错,那么体验会很糟糕。
我做了一个简单的三级兜底:
第一级:请求失败时,保留上次成功加载的数据,并显示一条 SnackBar 提示“网络异常,显示缓存数据”。 第二级:本地用shared_preferences保存最近一次成功拉取的数据,在冷启动时优先展示本地缓存,再静默拉取最新数据。 第三级:如果缓存也没有,显示一个完整错误页,给一个“重试”按钮。
这个设计让界面几乎永远“有东西可看”,不会出现纯白屏。前台就算知道网络卡了,也不至于手忙脚乱。
7.3 状态筛选器的交互细节
顶部状态筛选栏,我用的是一个横向滚动的ChoiceChip列表:
- 全部(默认选中)
- 待接车
- 检测中
- 维修中
- 待付款
- 已完工
选择某个状态时,列表数据即时过滤。这里注意一个技术细节:过滤不要直接在原始 List 上用where然后 setState,如果这么做,每次点击 Chip 都要遍历一次全量数据。我试过几十条数据的列表感觉不明显,但如果是几百条,会有瞬时卡顿。更好一点的方案是:筛选和搜索都交给后端(带 query 参数请求),但那意味着每次切换筛选条件都要重新请求接口,又会变慢。
最后我的方案是:数据量小(少于 200 条)时用本地过滤,超过 200 条才走后端筛选。门店场景下每班次同时在修车辆不会超过 60 台,本地过滤完全够用,体验也更顺畅。
8. 常见问题与排查技巧实录
8.1 真机调试时遇到的性能与渲染问题
问题一:列表滚动时掉帧。
现象:OpenHarmony 电视盒子上滚动维修卡片列表,FPS 明显偏低,严重时有白块闪烁。
排查与解决:先检查是不是卡片上的阴影太重,Card默认的 elevation 画实了很耗 GPU 性能。把 elevation 改成 1-2,或者改用Container + BoxDecoration模拟卡片。另外一个因素是卡片数量——如果页面一次性加载了 80 条维修数据,建议只保留“最近 20 条 + 状态未完成的”,历史数据放详情页里查。
问题二:首帧加载黑屏。
现象:App 冷启动时,OpenHarmony 设备显示黑色或空白页面,1-2 秒后才出现 Flutter 界面。
排查与解决:这是 Flutter 引擎初始化时间。优化手段包括:在 OpenHarmony 原生侧先显示一个简单的启动图(SplashScreen),等 Flutter attach 成功后移除启动图。同时检查 Flutter 仓库的调试模式——Debug 模式在 OpenHarmony 上首次启动会比较慢,Release 模式会好很多。
问题三:中文文本渲染发虚。
现象:在部分低端 OpenHarmony 设备上,中文文字边缘模糊。
排查与解决:Flutter 的文本渲染引擎在 OpenHarmony 上默认可能启用了某些抗锯齿效果,在低端 GPU 上反而变差。可以尝试在MaterialApp的theme中设置TextStyle的fontFamily为系统字体(例如 HarmonyOS Sans),并打开fontFallback。另外检查设备分辨率适配,有些盒子的默认 DPI 逻辑比较特殊。
8.2 编译与构建阶段的报错实战
问题四:hvigor 构建报node_module版本冲突。
现象:执行 hap 打包有时提示某些 npm 包的版本不存在或不匹配。
排查与解决:OpenHarmony 的构建依赖 Node.js 的某些包,版本兼容性非常微妙。我的做法是严格记录 DevEco Studio 的版本、Node 版本、SDK 版本三者组合,确定一组能跑的就不动它。新开工程时,先用官方模板构建成功一次,再集成 Flutter 模块,不要一上来就大改特改。
问题五:Flutter 引擎在 OpenHarmony 上 attach 失败。
现象:App 启动后 Flutter 页面始终无法渲染,原生页面正常。
排查与解决:原因是 Flutter 模块注册时机的问题。在 OpenHarmony 的 Ability 中,onCreate或onWindowStageCreate阶段要先完成 Flutter 模块注册,再加载 UI。如果顺序反了,就会 attach 失败或无法找到模块入口。这部分代码最好像 Android 原生那样,把 Flutter 模块的初始化逻辑集中在一个Engine生命周期回调里管理。
8.3 常见问题速查表
| 现象 | 可能原因 | 推荐解法 |
|---|---|---|
| 编译失败,提示 API 版本不匹配 | Flutter 适配分支与 OpenHarmony SDK 版本冲突 | 按官方 README 锁定版本组合 |
| 网络请求失败,无报错 | 缺少 INTERNET 权限声明 | 检查 module.json5 的 requestPermissions |
| 中文乱码 | 文件编码问题 | 统一 UTF-8,Dart 文件头部不要放多余 BOM |
| 列表滚动卡顿 | 列表项使用了高耗绘制(阴影、动画) | 降低阴影等级、关闭持续动画 |
| 状态不刷新 | Timer 在后台被系统回收 | 接入生命周期监听,回到前台时手动刷新 |
| 有些设备上文字被截断 | 屏幕适配/字体缩放导致溢出 | 卡片文本用 Flexible+ellipsis,注意 Row 内 Expanded |
| 真机日志看不到 Flutter 的 print | hilog 过滤级别不对 | 增加 flutter 关键字过滤,或调整 hilog 的日志级别 |
8.4 关于真机选择的一个建议
有条件的话,尽量用 OpenHarmony 真机来做联调,模拟器的渲染行为和真机差距不小。尤其像维修状态概览这种卡片密集型界面,模拟器流畅不代表真机流畅。我建议至少备两台设备:一台配置低一点的(模拟门店的电视盒子)和一台配置中等偏上的(模拟前台平板),这样高低两端都用过之后,性能和兼容性的底气就足了。
9. 从小 Demo 走向内部工具链的一点思考
很多类似界面最终沦为“演示专用”,因为一个大屏界面并不难做,难的是数据能不能稳定地从业务系统走到屏幕上。维修状态概览界面,表面上是 Flutter 的 UI 问题,实际上是一个“数据管道”问题——门店的业务系统(可能是老旧的 ERP)、接口服务、跨端 App,三者要打通。
在真实推行时,还会遇到一个容易被技术人忽略的问题:门店操作者愿不愿意用。维修技师忙着干活,不会刻意去更新状态,如果状态数据不及时,这个“概览屏”就会变成“忽悠屏”。所以一个完整方案,还需要在车间工位上布置一个简单的状态确认终端(比如一个按钮或者扫码枪),让技师顺手完成状态流转。
后续我想在这个界面上扩展的功能包括:
- 维修进度拍照上传,在修复的每个关键节点留档照片,客户可以远程查看。
- 超时预警,比如某个维修项目超过了预设时长,卡片自动高亮。
- 多门店切换,让区域经理能看到旗下多个门店的实时维修状态。
- 数据统计,比如单日完工车辆数、平均维修时长等指标,放在概览页顶部。
这些都是现有架构可以平滑扩展的——因为 Flutter 也好,OpenHarmony 也好,真正的成本在于第一公里的地基,地基打好了,后面加房间就容易了。
我个人在实际操作中的体会,做跨端项目最重要的不是会写 Flutter 控件,而是理解每一个平台的特性差异。OpenHarmony 不是一个“换壳 Android”,它在构建工具、权限声明、生命周期管理上都有自己的逻辑,把 Flutter 跑起来只是第一步,跑得稳、跑得顺、能落地,才是真正值得花时间打磨的地方。