几年前我就想找一套方案,能用同一份Flutter代码跑通OpenHarmony设备,这次借着城市井盖地图App的项目,总算把 flutter_for_openharmony 这套工具链从环境搭建到真机调试完整捋了一遍。整个过程踩了不少坑,也沉淀出一些高效做法。如果你也正在做 OpenHarmony 上的 Flutter 应用,或者对地图、外勤巡检这类智慧城市 App 感兴趣,这篇实战记录应该能帮你少走弯路。
这个项目本身不复杂,核心就是把全市井盖的位置、状态、巡检结果放到一张地图上,让外勤人员用手机就能完成日常巡查和报修。真正烧脑的地方在于:OpenHarmony 设备并不完全兼容 Android 生态,Flutter 官方 SDK 也没有直接支持 OpenHarmony,所以需要基于社区适配分支构建工程,还要自己处理地图组件、插件替换、开发者工具联动等一系列问题。下面按我的实际推进顺序,把可复现的步骤和代码一起写出来。
1. 项目背景与需求拆解:为什么用 Flutter 做 OpenHarmony 井盖地图
1.1 城市井盖管理的真实痛点
城市里的排水井盖、电力井盖、通信井盖数量庞大,一条主干道少说几十个,一个区可能上万个,全市范围内更是天文数字。过去巡检靠的是纸质台账和 Excel 表,外勤人员拿着打印出来的列表一个个去找,发现问题再用手机拍照、口头汇报,调度员人工登记后才能派维修单。整个过程有三个大问题:
- 数据滞后:井盖从破损到上报再到维修,中间可能隔几天,突发隐患无法及时暴露。
- 定位困难:井盖编号和地图坐标对不上,巡检员到了现场还得靠参照物估算。
- 闭环缺失:上报之后没有跟踪记录,谁也不确定维修是否完成,容易漏单。
这个项目要做的 App,就是要把井盖变成地图上的一个个可交互标记。打开 App 直接看到当前位置附近所有井盖,点击标记能看编号、状态、责任人、最后巡检时间;发现问题可以拍照上传,系统自动生成维修工单;地图上用红黄绿颜色区分紧急程度,让调度员一眼掌握全局。最终的效果类似共享单车找车,但数据结构更偏向政务工单系统。
1.2 选型:Flutter for OpenHarmony 的价值
项目启动时,团队内部讨论过两套方案:一是用 ArkUI 原生开发,二是用 Flutter 跨端方案。OpenHarmony 框架确实在成长,但 ArkUI 生态相对年轻,地图、图片、网络等三方库选择不多,而且市里已有的智慧城市管理后台大量逻辑是历史 Java/Web 代码,服务端接口大多是对接移动端定的,原生重新实现成本很高。
选 Flutter 的理由很实在:
- 一套 Dart 代码可以同时产出 OpenHarmony、Android、iOS 版本,后续如果要做多端部署,不用重写 UI 和业务逻辑。
- Flutter 的 UI 渲染一致性强,地图周边的是列表、表单、图表组件,不必依赖系统 WebView 的碎片化表现。
- 社区维护了 flutter_for_openharmony 适配项目,这是 OpenHarmony SIG 基于 Flutter 官方源码做的分支,持续同步版本,已经能支撑大部分场景。
当然,Flutter for OpenHarmony 不是百分百平滑,官方插件大多只适配了 Android/iOS,OpenHarmony 上要手动找替代方案。我在项目里做了一个妥协:地图这种强依赖原生能力的模块,不走原生 SDK,而是用 WebView 加载 H5 地图,Flutter 和 H5 之间通过 JSBridge 通信。这样既绕开了 OpenHarmony 对 Android 地图库的不兼容,又守住了在自己熟悉的技术框架内解决问题的底线。
1.3 功能清单与模块边界
在动手写代码之前,我先把需求拆成了一张表,后面开发按模块推进,避免陷入细节失控:
| 模块 | 功能说明 | 实现思路 |
|---|---|---|
| 地图总览 | 加载城市地图,展示井盖标记、图层切换 | Flutter WebView + H5地图JS API |
| 定位服务 | 获取GPS/网络定位,地图自动移动到当前位置 | WGS84坐标,通过MethodChannel调用系统定位 |
| 井盖信息 | 弹窗展示编号、类型、状态、负责人 | 本地SQLite缓存,网络数据同步 |
| 巡检上报 | 拍照、选择状态、提交表单 | image_picker + dio上传 |
| 消息告警 | 接收故障工单,本地通知提醒 | 轮询接口 + 本地通知 |
| 离线缓存 | 弱网下仍可查看历史数据和上报记录 | sqflite_ohos + 同步队列 |
| 开发者工具 | 调试面板、坐标模拟、状态注入 | Flutter Overlay + 自研Logger |
模块边界清晰之后,开发节奏明显加快。后面每个环节我都会单独讲实操细节。
2. 开发环境配置与开发者工具链搭建
2.1 环境准备:flutter_flutter 与 DevEco Studio
OpenHarmony 的 Flutter 开发不能直接用flutter.dev官方 SDK,至少现阶段不行。需要使用社区维护的flutter_flutter分支,同时配合华为的 DevEco Studio 完成设备侧的编译和运行。
我的环境版本供参考:Ubuntu 22.04 开发机、OpenHarmony 3.2 Release 真机、DevEco Studio 4.0(下载时选最新稳定版)、Node.js 18。
搭建步骤如下:
- 克隆 OpenHarmony SIG 的 Flutter 分支,并切换到 ohos 分支:
git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git export PATH=$PATH:$PWD/flutter_flutter/bin flutter doctor - 安装 DevEco Studio,并配置 OpenHarmony SDK。打开 DevEco Studio,在设置里指定 SDK 路径,通常会自动识别。
- 安装 hdc 工具。OpenHarmony 的命令行调试工具没在全局 PATH 里,需要找到 SDK 目录下的 toolchains,例如
deveco-studio/toolchains/hdc或ohos-sdk/linux/toolchains/hdc,添加到环境变量。
这一步有个常见的坑:如果你本机之前装过官方的 Flutter SDK,再切到 flutter_flutter 分支会冲突。因为 flutter 命令会把两个 SDK 都识别到,导致版本判断异常。建议用一个独立的OPENHARMONY_FLUTTER_ROOT环境变量,把flutter命令的优先级提到最前,或者直接单独建一个用户别名指向 ohos 分支。
2.2 用 DevEco Studio 创建并连接 OpenHarmony 设备
创建工程时,我用了 Flutter 模板命令,因为纯 ArkUI 模板没有 Dart 目录结构:
flutter create --platforms ohos manhole_map_app cd manhole_map_app命令执行完,会生成一个同时包含ohos/目录和lib/目录的混合工程。ohos/是 OpenHarmony 的模块配置,lib/是 Flutter 的 Dart 代码。接下来用 DevEco Studio 打开这个目录,它会自动识别 Flutter 模块。
真机调试之前,必须在 DevEco Studio 里完成签名配置。点击File > Project Structure > Signing Configs,勾选自动生成签名,并登录华为开发者账号。没有签名的话,App 无法安装到设备上,报的错误是install sign verify failed。
连接设备后,运行方式有两条路:
- 在 DevEco Studio 工具栏点击绿色运行按钮,选择 OpenHarmony 设备,它会自动调用 hdc 安装并启动应用。
- 也可以命令行执行
flutter run -d <deviceId>,其中设备 id 用hdc list targets获取。
我在项目里更习惯用命令行加日志过滤的组合,因为 Flutter 在 OpenHarmony 上的输出日志和 DevEco Studio 的日志窗口偶尔不同步,命令行能抓到更完整的异常堆栈。
2.3 开发者工具的“再实现”:内嵌调试面板
说到开发者工具,我的理解分两层。一层是 IDE 本身,比如 DevEco Studio、Flutter DevTools,它们是标准链路;另一层是业务研发过程中缺失的“现场调试工具”——比如在真机上快速查看 FPS、内存、接口耗时、当前坐标,甚至往地图里塞一个模拟井盖。官方 DevTools 在 OpenHarmony 适配度有限,所以我干脆做一个 Flutter Overlay 调试面板,嵌入到 App 中。
实现思路是:在主界面根 Widget 上叠加一个可拖动的悬浮面板,用show()或hide()控制显隐。核心代码如下:
class DebugOverlay extends StatefulWidget { const DebugOverlay({super.key}); @override State<DebugOverlay> createState() => _DebugOverlayState(); } class _DebugOverlayState extends State<DebugOverlay> { Timer? _timer; int _frameCount = 0; double _fps = 0; DateTime _lastTime = DateTime.now(); @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { setState(() { _fps = _frameCount.toDouble(); _frameCount = 0; }); }); WidgetsBinding.instance.addPostFrameCallback((_) => _tick()); } void _tick() { _frameCount++; WidgetsBinding.instance.addPostFrameCallback((_) => _tick()); } @override Widget build(BuildContext context) { return Positioned( right: 12, top: 120, child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(8.0), child: Text('FPS: $_fps'), ), ), ); } }这个面板还能塞很多自定义数据,比如最近一次接口请求的耗时、当前地图中心经纬度、定位状态等。它背后的价值在于:项目进入真机演示环节时,不需要插电脑看 DevTools,直接打开 App 右上角的面板就能定位性能问题。这个小工具我在内部会议上演示过一次,效果比截图日志好得多。
3. 城市井盖地图 App 核心功能实现
3.1 地图组件接入:从原生 SDK 到 WebView
最开始我想直接集成高德地图 Flutter 插件,一查发现 OpenHarmony 不支持 Android 构建产物,Flutter 插件又是通过 Gradle 加载 Android SDK,这条路基本走不通。后来换成 WebView 方案:Flutter 内嵌一个全屏 WebView,加载高德地图 JS API 的 H5 页面,地图渲染完全交给 H5,Flutter 负责业务层和数据层。
为什么不担心 WebView 性能?因为地图瓦片加载、手势缩放本来就在 WebView 里有成熟方案,Flutter 只接收 H5 传来的事件和调用 H5 的 JS 方法,开销很小。井盖数量即使到十万个,H5 页面用聚类插件也能承受。
核心代码是 Flutter 侧的 WebView 封装:
import 'package:webview_flutter/webview_flutter.dart'; class MapPage extends StatefulWidget { const MapPage({super.key}); @override State<MapPage> createState() => _MapPageState(); } class _MapPageState extends State<MapPage> { late final WebViewController _controller; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel('FlutterBridge', onMessageReceived: (message) { _handleJsMessage(message.message); }) ..loadRequest(Uri.parse('https://your-server.com/map.html')); } void _handleJsMessage(String json) { final data = jsonDecode(json) as Map<String, dynamic>; if (data['type'] == 'marker_click') { final id = data['id'] as String; // 跳转井盖详情页 } } @override Widget build(BuildContext context) { return Stack( children: [ WebViewWidget(controller: _controller), // 顶部的返回按钮和图层切换按钮 ], ); } }H5 页面里的关键桥接代码则是:
window.FlutterBridge.postMessage(JSON.stringify({ type: 'marker_click', id: manholeId, lat: lat, lng: lng }));这样 Flutter 和 H5 之间的通信就形成了一条单向数据流:点击标记 -> JS 采集数据 -> Flutter 解析 -> 跳转详情。如果要让 Flutter 主动调 H5,比如地图移到某个经纬度,就用_controller.runJavaScript("window.moveTo($lat, $lng)")。JSON 格式是双方约定好的接口协议,比拼接字符串更安全。
3.2 井盖数据模型与本地缓存
地图上的井盖数据来自服务端接口,但外勤人员在桥洞、地下通道等弱网场景经常没信号,所以必须做本地缓存。我选的是sqflite_ohos,它和sqfliteAPI 基本一致,只是底层适配了 OpenHarmony 的 SQLite 接口。
建表语句:
CREATE TABLE manholes ( id TEXT PRIMARY KEY, code TEXT NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL, type TEXT, status TEXT, address TEXT, images TEXT, last_inspect_time TEXT, updated_at INTEGER );Dart 模型和 DAO 层不需要整段贴,这里说一个关键点:经纬度存储不要用字符串拼接,统一用 REAL 类型。我见过有些老系统把经纬度存成 VARCHAR,导致地图排序和范围查询全部失效。ORM 层用toMap()和fromMap()做转换,写入时注意类型转换:
class Manhole { final String id; final String code; final double lat; final double lng; final String status; Map<String, dynamic> toMap() => { 'id': id, 'code': code, 'lat': lat, 'lng': lng, 'status': status, }; factory Manhole.fromMap(Map<String, dynamic> map) => Manhole( id: map['id'] as String, code: map['code'] as String, lat: (map['lat'] as num).toDouble(), lng: (map['lng'] as num).toDouble(), status: map['status'] as String, ); }缓存策略采用“首次全量下载 + 增量更新 + 离线写入队列”。App 启动时拉取一次全量井盖写入本地,之后每隔十分钟请求updated_at大于本地最新时间的变更记录。巡检员离线状态提交的上报数据会先写入pending_reports表,等网络恢复后统一同步。这也是这个 App 能在外勤场景真正落地的关键。
3.3 巡检上报与告警闭环
巡检流程其实很像电商下单:选商品 -> 加购物车 -> 提交订单,这里则是选井盖 -> 拍照 -> 提交工单。代码上我用image_picker_ohos调起相机,用dio做网络请求。
拍照部分:
final picked = await ImagePicker().pickImage( source: ImageSource.camera, maxWidth: 1280, maxHeight: 1280, imageQuality: 70, ); if (picked != null) { final bytes = await picked.readAsBytes(); final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes(bytes, filename: '${DateTime.now().millisecondsSinceEpoch}.jpg'), }); final resp = await dio.post('/api/upload', data: formData); final imageUrl = resp.data['url']; }这里我特意限制图片尺寸到 1280 和 70% 质量,因为外勤人员手机型号杂,一张 5000 万像素照片上传会卡死弱网。图片压缩后再上传,服务端保留原图也可以,但移动端体验优先。
巡检测到异常井盖后,App 除了提交服务端,还要在本地触发一个告警状态。我这边用状态机维护:正常运行 -> 待维修 -> 维修中 -> 已修复。当某个井盖被上报为“破损”或“缺失”,地图上标记颜色立刻变红,同时在设备上弹出一条本地通知。通知的文案模板是固定的:
- 破损:
井盖${code}破损,请尽快现场复核 - 缺失:
井盖${code}缺失,存在安全隐患,请立即处理
告警消息没有接第三方推送,而是做了一个 30 秒轮询接口,因为政务内网环境往往不支持公网厂商推送。轮询接口只返回未处理工单数量,消息体很小,功耗也可控。
4. 实测中遇到的问题与排查技巧实录
4.1 Flutter 插件在 OpenHarmony 上的兼容性问题
开发过程中最大的敌人不是 Dart 语法,而是插件生态。下面这张表是我的真实踩坑记录,供参考:
| 插件 | Android原生能力 | OpenHarmony适配度 | 我的替代方案 |
|---|---|---|---|
| path_provider | 获取沙盒路径 | 需要 path_provider_ohos | 改用方法通道自实现 |
| shared_preferences | 本地键值存储 | 有 shared_preferences_ohos | 改用 Preferences 缓存 |
| image_picker | 拍照/相册选择 | 社区有 image_picker_ohos | 或自己用 camera 插件包一层 |
| webview_flutter | 网页视图 | 官方支持有限 | 用平台自带的 WebView |
| dio | 网络请求 | 纯 Dart 实现,无需适配 | 直接使用 |
关键原则是:尽量把依赖圈在纯 Dart 插件内。比如 dio 是纯 Dart,用起来非常稳;而url_launcher这种依赖原生 API 的,就可能出现调用时崩溃。解决办法是自己写一个MethodChannel桥接,在 OpenHarmony 原生侧实现对应功能。
4.2 构建和运行报错速查表
我给参与这个项目的同事整理过一张速查表,很多问题报错信息看起来很吓人,实际原因就那么几种:
| 报错现象 | 根本原因 | 解决办法 |
|---|---|---|
libflutter.so not found | 设备系统库版本与 Flutter SDK 版本不匹配 | 升级设备OpenHarmony版本或降低flutter_flutter分支版本 |
CMake Error: openharmony sdk not found | DevEco Studio里的SDK路径未配置 | 在local.properties中显式设置ohos.sdk.dir |
MethodChannel error: NotImplemented | 插件未支持OpenHarmony | 换成纯Dart实现或用MethodChannel自研 |
sign verify failed | 签名文件缺失或过期 | 重新到DevEco Studio生成签名 |
| 地图页面白屏 | WebView加载了HTTP地址被安全策略拦截 | 地图URL改为HTTPS,并配置WebView允许混合内容 |
| 真机热重载无效 | OpenHarmony当前分支对热重载支持有限 | 用flutter run重启,或手动触发r键 |
4.3 避坑心得:先跑通最小闭环
如果你第一次接触 flutter_for_openharmony,我强烈建议不要直接把这个项目当起点。先建一个空工程,只写一个 Text 按钮和一张原生图片,确认三件事:
- Flutter 页面能不能正常渲染到 OpenHarmony 屏幕。
- MethodChannel 能不能调起一个原生 Toast。
- 传感器、定位等复杂 API 是否可用。
我把这个“三连确认”称为最小闭环。如果最小闭环跑不通,后面所有功能都白搭。项目中期,我因为没确认第 2 步,花了两天时间排查为什么image_picker调不起相机,最后发现是插件在 OpenHarmony 上根本没有注册原生端,调方法时直接抛异常。这个教训提醒我:每接入一个新插件,先写一个最小验证页面,跑通了再进业务代码。
5. 后期扩展与个人体会
5.1 从井盖扩展到更多智慧城市场景
项目完成后,我又把它复用了几个场景,结构几乎不用动。路灯管理:地图上的标记换成路灯编号,状态改为“亮/灭/故障”;垃圾桶满溢监测:上报照片改成传感器回调;消防栓漏水:告警逻辑改成压力阈值。
核心改动只有数据模型和 H5 地图的 marker 图标,Flutter 层的地图容器、消息通道、缓存队列全部复用。这就是跨端架构的红利,尽量把通用能力抽象出来,平台细节往底层收敛。
后续如果要做更专业的离线地图,我会把 WebView 方案替换成 MapLibre 渲染矢量瓦片,配合本地 GeoPackage 数据,做到完全离线也能显示井盖。但这要等 flutter_for_openharmony 对 Canvas 的性能支持更成熟后再动手。
5.2 一些真实经验
这次项目做下来,我对 OpenHarmony + Flutter 的组合看法是:可以干,但要降低预期。它适合快速产出界面复杂的跨端应用,却不适合做依赖大量系统私有 API 的应用。开发周期里大概有三分之一的精力花在找插件替代方案上,这是生态早期的必然成本。
如果你也在做类似项目,我有几个建议:
- 当前阶段优先选 WebView + HTTP + SQLite 组合,这三个模块在 OpenHarmony 上都算稳定。
- 开发者工具不能只依赖 IDE,项目团队最好维护一个自己的调试面板,把线上数据注入、日志导出、FPS 监测集成到 App 内部,这样现场演示和远程问题定位都从容很多。
- 遇到问题先搜 OpenHarmony SIG 的 issue,这个项目的排错经验往往比 Stack Overflow 上的更贴切。
最后分享一个汇报利器:我在调试面板里加了一个“模拟井盖”按钮,长按地图就能随机生成一个井盖,并随机赋予“破损”或“完好”状态。演示时一键放了几百个模拟点,地图瞬间丰满起来,领导和客户都很直观地看到功能效果。这个设计原本是为了测试性能,意外成了项目汇报的加分项。
Flutter for OpenHarmony 还在快速演进,可能你看文章时它又发布了新版本,但底层的适配思路和开发流程不会变:环境先行、最小闭环、插件精选、工具自主。这套方法论不仅能支撑井盖地图,也能支撑绝大多数 OpenHarmony 跨端应用。