城市井盖这个东西,平时没人盯着,一旦缺失或者破损,影响的就是车辆轮胎和行人安全。我最近用 Flutter for OpenHarmony 做了一个“城市井盖地图 App”的实际练手项目,把井盖按状态标在地图上,支持筛选查看和位置上报,还顺手把偏好设置这一块仔仔细细做了一遍。整个过程跑完,我对 OpenHarmony 上做 Flutter 应用的真实难度、地图组件怎么桥接、设置数据怎么持久化,都有了比较具体的答案。想踩 Flutter + OpenHarmony 这条路、又需要地图和本地配置功能的开发同学,这篇文章大概率能帮你少走不少弯路。
项目本身不算大,但地图展示、状态点标注、列表筛选、设置页面这些模块一个都不少,更像一个小型物联网资产管理应用的前端雏形。我把它拆成“地图主界面 + 井盖状态图层 + 上报表单 + 偏好设置”四块,接下来会按实战顺序讲,不会只贴代码,更多的会聊“为什么这么设计”和“真机上到底会遇到哪些事”。
1. 项目背景与整体设计思路
1.1 项目定位:一张“会更新”的井盖地图
城市井盖管理的真实痛点差不多是这个样子:井盖产权单位多、分布广、状态变化快,而且每一个井盖的生命周期都值得被记录。有的井盖在主干道上,车流密集,每天巡查成本很高;有的在背街小巷,出了异常很难被及时发现。如果有一张地图能把这些井盖按位置和状态展示出来,再配合一个简单上报入口,巡查人员或者热心用户看到破损井盖就能拍照上传,维护团队也能按位置快速到场,效率会提升不少。
这个 App 的核心不是做一个复杂的 GIS 系统,而是用最直接的方式解决三个问题:井盖在哪、状态如何、最近有没有人上报过。所以我在设计功能时没有引入太多重型框架,地图用原生地图组件承载,业务层用 Flutter 写,数据层用轻量本地库加简单 JSON 文件即可。对于入门的 OpenHarmony 应用项目来说,这个规模刚刚好,既能练到桥接和状态管理,又不会把自己拖进过深的工程泥潭。
1.2 技术选型:为什么是 Flutter for OpenHarmony
在接触 OpenHarmony 之后,我第一反应是和大多数跨端开发者一样:能不能直接用 Flutter 写?Flutter 的渲染引擎天然跨平台,Dart 语法写业务逻辑效率也高,UI 表现力比部分原生声明式方案更可控。而 Flutter for OpenHarmony 这条社区路线,说白了就是把 Flutter 引擎跑在 OpenHarmony 系统之上,让现有 Flutter 开发经验能够平移到鸿蒙生态里来。
选择 Flutter for OpenHarmony 还有一层实际考量:团队里如果有 Android 或已有 Flutter 经验的人,学习成本会低很多。相比完全用 ArkTS 重写一套业务,Flutter 侧能复用的代码量非常可观。比如本项目里的井盖状态枚举、数据模型、偏好设置读取逻辑、Provider 状态管理,这些几乎是从原有 Flutter 项目直接拿过来就能用。真正要额外处理的,是 OpenHarmony 侧的引擎接入、原生地图组件桥接和系统权限申请。
不过也要说清楚,Flutter for OpenHarmony 并非所有 Flutter 插件都能直接用。很多 pub.dev 上的插件默认只实现了 Android 和 iOS 的原生代码,要在 OpenHarmony 上跑,就得看有没有对应的 Ohos 适配实现。这个项目里最典型的就是地图 SDK 和偏好设置组件,我后面专门用两章来讲怎么绕过这些限制。
1.3 功能拆解与界面规划
我习惯在动工之前先把功能收敛,避免越做越散。这个井盖地图 App 最后定的功能分成四个页面,每个页面职责都很明确:
- 首页地图:展示当前定位、加载井盖数据、根据不同状态用不同颜色打点,点击 Marker 弹出井盖详情。
- 井盖上报:填写位置、选择状态、补充现场照片,提交后刷新地图。
- 消息列表:简单记录系统通知或附近井盖上报警告,方便测试偏好设置里的通知范围。
- 设置页:管理地图显示偏好、筛选条件、上报人默认信息、通知范围等。
这里最值得花心思的是“首页地图”和“设置页”之间的联动。比如用户在设置里勾选了“只显示破损和缺失井盖”,回到主页时地图上的 Marker 就要立刻刷新;用户在设置里改深色模式,地图底图风格和全局主题也要一起变。这些联动听起来普通,但如果没有提前规划偏好设置的数据流,写到最后大概率会出现状态不同步、界面和实际存储不一致的坑。
2. 环境与工程搭建:Flutter for OpenHarmony 项目第一次跑通
2.1 版本选择:先把 SDK 对齐,不然全是坑
做 OpenHarmony 上的 Flutter 开发,第一步并不是急着写代码,而是把手上的版本理清楚。我碰上过一个非常头疼的情况:Flutter SDK 用的是社区 3.7 的适配分支,OpenHarmony SDK 却配到了 API 10,结果编译 Flutter 引擎时反复出现符号找不到的问题,后面换成与分支文档匹配的 API 9 设备镜像才稳定下来。所以先确认三件事:Flutter 适配分支版本、OpenHarmony SDK 版本、开发板系统版本,这三者最好按照工程 README 里验证过的组合来配,不要自己混搭。
OpenHarmony 设备的版本差异也要注意。系统镜像有标准系统、小型系统和轻量系统之分,“城市井盖地图”这种带地图和定位的功能,必须跑在标准系统上。API Level 越高,权限模型和后台任务限制越严格,如果你的 Flutter 适配分支较老,反而可能在新系统上出现兼容性 bug。我的建议是前期用官方适配分支推荐的那套系统版本跑通全流程,后面再考虑升级。
2.2 创建一个 Flutter for OpenHarmony 工程
这部分很多文章一笔带过,但实际操作有细节。假如你已经拿到了 flutter_for_openharmony 的 SDK,并把它放到了 Flutter 的可执行路径里,创建工程时依然用flutter create,只是最后的工程里会额外生成一个适配 OpenHarmony 的原生工程目录。这个目录名称在不同版本里不完全一样,常见的是ohos,打开后类似一个独立的 OpenHarmony 应用工程。
比较关键的一步是包名配置。Flutter 侧的 applicationId 和 OpenHarmony 的 bundleName 并不是一回事,一定要在原生工程配置里建好映射关系。否则安装到真机时会出现应用包名冲突,或者 MethodChannel 访问不到原生代码。我第一次就忽略了这个问题,导致 debug 包能装上但原生 Plugin 找不到入口,排查了整整一天。
工程创建后,先别急着加地图功能,先跑一个空页面真机验证。用数据线连接开发板,确保 OpenHarmony 的 hdc 工具能识别设备,然后执行运行命令。如果日志里能看到 Flutter 的启动 banner,说明 Flutter 引擎在 OpenHarmony 上已经正常跑起来了。这一步稳定后,后面的桥接工作才有基础。
2.3 开发板怎么选:rk3568 和 rk3588 到底选谁
热词里大家都在问 rk3568 和 rk3588 的设备树怎么选,这里我先给一个应用层开发者的结论:如果你只是在官方镜像之上安装 Flutter 应用,根本不需要碰设备树。设备树是内核和系统编译阶段的概念,普通应用开发接触不到这一层。会问这个问题,多半是看到了自己编译 OpenHarmony 系统源码的教程,或者在群聊里被术语唬住了。
但如果真的要自己裁剪系统,那就要按照开发板的型号去选对应 dts 文件。rk3568 的常用开发板对应的是 rk3568-evb 或具体厂商命名的 dts,rk3588 对应的是 rk3588 系列 evb 配置。选错设备树轻则外设驱动不工作,重则系统起不来。以 Flutter 开发为目的的话,我更推荐先选 rk3568 的 DAYU200 这类资料齐全的开发板,网上能搜到的烧录教程和 bug 解决方案最多。rk3588 性能更强,但如果你只是想验证 Flutter 应用逻辑,性能差异其实感知不明显。
2.4 真机权限申请:位置和相机一个都不能少
地图类 App 离不开定位权限,上报功能需要相机权限。OpenHarmony 的权限模型和 Android 有相似之处,但接口并不一样。在原生工程配置文件里声明权限只是第一步,运行时还需要主动向用户申请。如果漏了运行时申请,直接去调定位或者打开相机,可能会回调一个权限拒绝错误,但界面上没有任何弹窗提示,这种问题最坑。
建议在 Flutter 侧做一个统一的权限服务,将原生权限申请能力用 MethodChannel 暴露给 Dart。进入首页地图时先申请定位权限,进入上报页时再申请相机权限。不用一次性申请完所有权限,这样用户的授权意愿更高,也符合最小化授权原则。权限回调结果要同步回 Dart,避免出现弹窗还在显示,界面却已经跳转的竞态问题。
3. 地图模块实战:把井盖“钉”到地图上
3.1 地图容器的接入思路:PlatformView 绕不开
OpenHarmony 生态里的地图 SDK 和 Android 生态并不完全互通,所以最稳妥的做法是在 OpenHarmony 原生侧创建地图视图,再通过 Flutter 的 PlatformView 机制嵌入 Flutter 页面。简单说,地图依然由原生 SDK 渲染,Flutter 只负责包围它,并在外部叠加按钮、弹窗等 UI,两边的通信走 MethodChannel。
Dart 侧的地图容器组件,核心是注册一个平台视图类型,比如ohos_maps。这里有一个容易踩的点:不同 Flutter for OpenHarmony 分支对 PlatformView 的注册方式略有差异,有的沿用registerViewFactory,有的需要换到PlatformViewRegistry的具体实现。我的经验是先找到分支内置的示例工程,照着它的调用方式改,别硬搬 Android 文档里的旧写法。
Dart 侧代码大致长这样:
Widget build(BuildContext context) { return PlatformViewLink( viewType: 'ohos_maps', onCreatePlatformView: (params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: 'ohos_maps', layoutDirection: TextDirection.ltr, creationParams: { 'centerLatitude': _centerLatitude, 'centerLongitude': _centerLongitude, 'zoom': 15, }, creationParamsCodec: const StandardMessageCodec(), onFocus: () {}, ); }, ); }注意这只是一个面向 Flutter 标准接口的写法,OpenHarmony 分支是否完全兼容initSurfaceAndroidView要以实际引擎为准。如果项目里已经有 Android 地图开发经验,会发现整体思路非常接近:本质上都是把原生 View 包装成一个 Flutter 可以承载的控件。
3.2 原生侧地图初始化和 Marker 管理
地图真正能不能用,原生侧是主角。在 OpenHarmony 原生侧初始化地图 SDK 后,需要提供几个基础方法给 Flutter 调用:设置中心点、缩放、添加 Marker、移除 Marker、镜头移动回调。这几个方法基本可以覆盖井盖地图 80% 的业务需求。
添加 Marker 时,不建议 Flutter 每画一个点就调用一次原生方法。地图 Marker 数量如果上了几十上百,频繁跨通道通信容易丢帧。更好的方式是一次性把 Marker 列表序列化成 JSON 传给原生侧,原生侧批量解析并添加。回调则反过来,原生侧把点击的 Marker ID 通过通道抛回 Flutter,Flutter 再根据 ID 找到井盖详情。
原生侧方法调用示例可以理解为这样:
// 伪代码,实际接口随地图 SDK 而定 mapController.addMarkers([ { 'id': 'MH20240001', 'latitude': 31.2304, 'longitude': 121.4737, 'icon': 'broken', }, ]); mapController.setOnMarkerClickListener((markerId) => { channel.invokeMethod('onMarkerTap', {'id': markerId}); });井盖状态决定图标颜色,这个逻辑放在 Dart 侧算好,原生侧只负责按类型取对应图标。这样地图 SDK 和业务模型解耦,以后即使换地图厂商,Flutter 侧几乎不用改。
3.3 井盖数据模型设计
井盖信息看着简单,真正建模时才意识到“状态”这个字段会一路影响地图筛选、列表展示和偏好设置。我给井盖定义了几个关键字段:井盖编号、所在道路、经纬度、状态、上报人、上报时间、最后维护时间。
enum ManholeStatus { normal, broken, missing, repairing } class ManholeCover { final String id; final String road; final double latitude; final double longitude; final ManholeStatus status; final String reporter; final DateTime updatedAt; const ManholeCover({ required this.id, required this.road, required this.latitude, required this.longitude, required this.status, required this.reporter, required this.updatedAt, }); factory ManholeCover.fromJson(Map<String, dynamic> json) { return ManholeCover( id: json['id'] as String, road: json['road'] as String, latitude: (json['latitude'] as num).toDouble(), longitude: (json['longitude'] as num).toDouble(), status: ManholeStatus.values.firstWhere( (e) => e.name == json['status'], orElse: () => ManholeStatus.normal, ), reporter: json['reporter'] ?? '', updatedAt: DateTime.tryParse(json['updatedAt'] ?? '') ?? DateTime.now(), ); } }这个模型对应 JSON 时要注意经纬度类型。Dart 里解析 JSON 得到的是 num,不能直接强转 double,否则会抛类型异常。状态字段推荐用字符串枚举名,这样在地图端做筛选和上报端提交时,语义都比较清晰。
3.4 地图图层刷新与筛选逻辑配合
地图上展示哪些井盖,不是一个简单的“把数据导进去”过程,而是要满足用户偏好。例如设置里勾选了“只显示非正常状态”,那么地图层就要把状态为 normal 的井盖过滤掉。由于我上面说了 Marker 是批量添加的,所以这里的动态刷新逻辑也要走批量更新:Flutter 把新的井盖列表发给原生侧,原生侧先清空旧 Marker,再添加新 Marker。
清空重建 Marker 虽然高效,但也有一个体验问题:每次刷新地图视角会轻微抖动。优化方案是区分“新增”和“全量替换”。第一次加载或者用户切换筛选条件时做全量替换,而上报完成后只对新增的一个 Marker 做单点插入,这样视觉上更平滑。如果你的地图 SDK 支持批量增删,那自然更省事。
另外,设置里的“地图默认缩放级别”和“是否跟随定位”这些偏好,会直接影响地图初始化参数。正确做法是在地图创建前先把偏好读出来,而不是地图已经显示到默认城市后再跳一次。App 冷启动时尤其重要,一闪而过的默认城市再跳转到当前定位,会让人觉得不太聪明。
4. 偏好设置实现:不是简单存几个 Switch
4.1 需求盘点:设置页到底该放哪些东西
大多数 demo 里的偏好设置就是几个开关,存一个布尔值完事。实际做成井盖地图后,我发现偏好设置至少分成三类。
第一类是显示偏好,包括地图深色模式、默认缩放级别、是否跟随定位、是否显示正常井盖。这类设置直接影响地图首页的初始状态和刷新逻辑。第二类是业务偏好,比如上报人默认姓名、默认联系电话、上报图片压缩质量。这能让日常重复上报操作少打几个字,非常实用。第三类是提醒偏好,比如附近异常井盖提醒的半径,用户希望关心多大范围由自己定义。
把这些需求整理成一张表格,后端存储字段就非常清楚了:
| 偏好项 | 类型 | 默认值 | 生效时机 |
|---|---|---|---|
| 深色模式 | bool | false | 全局立即生效 |
| 仅显示异常井盖 | bool | false | 回到地图页立即生效 |
| 地图跟随定位 | bool | true | 地图初始化时生效 |
| 异常告警半径 | double | 500(米) | 设置页实时预览 |
| 默认上报人姓名 | string | 空 | 上报表单自动填入 |
| 默认联系电话 | string | 空 | 上报表单自动填入 |
4.2 存储层选型:SharedPreferences 到底能不能用
在普通 Flutter 应用里,存设置首选是shared_preferences插件,但在 OpenHarmony 适配版本里并不一定能拿到现成的实现。如果你搜索过,会看到几种方案并存:有人用一个特定的 openharmony 适配包,有人在原生侧写死了几个 key,还有人干脆把配置写到本地文件里。
我最后的做法是:自己封装一个轻量存储通道,原生侧使用 OpenHarmony 的 Preferences 能力,Dart 侧只暴露 setString、getString、remove 几个异步方法。这样既不依赖第三方适配包是否完善,也让后续迁移到其他存储方式变得非常容易。OpenHarmony 的 Preferences API 本身也是以键值对形式存储,和 SharedPreferences 的使用习惯十分接近,学习成本相当低。
原生侧 ArkTS 核心逻辑类似这样:
import preferences from '@ohos.data.preferences'; import { common } from '@kit.AbilityKit'; let pref: preferences.Preferences | null = null; async function loadPref(context: common.Context) { if (pref === null) { pref = await preferences.getPreferences(context, 'manhole_map_prefs'); } return pref; } export async function putString(context: common.Context, key: string, value: string) { const store = await loadPref(context); await store.put(key, value); await store.flush(); } export async function getString(context: common.Context, key: string) { const store = await loadPref(context); return store.get(key, '') as string; }当然不同 OpenHarmony 版本里@ohos.data.preferences的包名和接口可能有细微差异,以你使用的 SDK 文档为准。我这里想强调的是设计思路:Flutter 侧不需要关心 Preferences 内部实现,只认字符串键值对。
4.3 Dart 侧偏好服务封装:缓存优先,异步落盘
Dart 侧不能每次读设置都跨通道去取,那样不但慢,还会把代码搞得很难看。我的方案是做一个PreferenceStore单例,启动时一次性把配置读入内存,后面所有读取都走内存,写入时更新内存并异步调用原生侧保存。
这里推荐把设置项收敛成不可变对象。比如定义一个AppPreferences类,包含上面表格里的字段,再提供一个copyWith方法。这样设置页修改任何一项,都能生成一个新的AppPreferences对象,避免散落一堆单独 get/set 方法难以维护。
class AppPreferences { final bool darkMode; final bool onlyShowAbnormal; final bool followLocation; final double alertRadius; final String reporterName; final String reporterPhone; const AppPreferences({ this.darkMode = false, this.onlyShowAbnormal = false, this.followLocation = true, this.alertRadius = 500, this.reporterName = '', this.reporterPhone = '', }); AppPreferences copyWith({ bool? darkMode, bool? onlyShowAbnormal, bool? followLocation, double? alertRadius, String? reporterName, String? reporterPhone, }) { return AppPreferences( darkMode: darkMode ?? this.darkMode, onlyShowAbnormal: onlyShowAbnormal ?? this.onlyShowAbnormal, followLocation: followLocation ?? this.followLocation, alertRadius: alertRadius ?? this.alertRadius, reporterName: reporterName ?? this.reporterName, reporterPhone: reporterPhone ?? this.reporterPhone, ); } }这样写有另一个好处:设置页可以先把所有修改集中在一个临时副本上,等用户点了“保存”再统一写入存储;也可以每个控件变更后立即保存并通知全局,体验上更像真实 App。井盖地图里我采用的是后者,因为像“仅显示异常井盖”这种开关,用户希望拨一下再回到地图就能看到结果,不存在“先改动后确认”的心理预期。
4.4 设置界面与全局状态同步
设置页的 UI 本身不难,Flutter 里的SwitchListTile、CheckboxListTile、Slider、TextField都是现成的轮子。这里有一个经常被忽视的细节:CheckboxListTile的标题和复选框之间的间距会受到controlAffinity属性影响,想调成“标题靠左、复选框靠右”的常见列表样式,就把controlAffinity设置为ListTileControlAffinity.trailing,同时用contentPadding控制左右边距。
设置状态同步我建议用一个 ChangeNotifier 实现,不需要立刻引入上层的复杂状态库。定义一个PreferenceController,持有当前AppPreferences,有一个update(AppPreferences newPrefs)方法,调用时同时做三件事:
class PreferenceController extends ChangeNotifier { AppPreferences _prefs; final PreferenceStore _store; PreferenceController(this._prefs, this._store); AppPreferences get prefs => _prefs; Future<void> update(AppPreferences newPrefs) async { _prefs = newPrefs; notifyListeners(); await _store.save(newPrefs); } }三件事分别是:更新内存对象,通知所有监听者刷新界面,异步持久化到原生 Preferences。这里的顺序也很重要:先通知界面再异步保存,用户手感和数据安全能兼顾。如果把await _store.save()放在notifyListeners()之前,一旦原生侧写盘慢,界面就会卡住像是没点中开关。
全局主题对darkMode的响应,我是在 MaterialApp 外层监听PreferenceController,动态切换theme和darkTheme。地图底图的深色模式则是通过 MethodChannel 通知原生地图切换风格。这里需要再次强调:界面主题和地图风格一定要同时切换,否则会出现设置页已经是深色、地图还是亮白色的割裂感。
4.5 上报表单与偏好数据自动填充
偏好设置的另一个价值体现在上报表单里。市政巡查员每天可能要上报几十个井盖,每次手动输入姓名电话非常不友好。我的做法是在上报页面初始化时读取PreferenceController,如果发现reporterName和reporterPhone不为空,就自动填入表单。表单提交成功后,如果用户勾选了“记住本次填写信息”,就把最新填写的姓名和电话回写到偏好设置。
这看起来很简单,但有一个细节:用户点击上报按钮到页面销毁,中间有异步保存的过程。如果你在提交后立即Navigator.pop,页面还没等保存完成就没了,可能出现设置没写进去的问题。稳妥做法是先执行偏好保存,再执行导航返回,或者在控制器里做事务性提交。我习惯是把它串成一个异步方法,保存成功后再关页面,同时在界面上给出一个轻量 loading 遮罩,避免用户重复点击。
5. 真机调试中踩过的几个坑
5.1 地图手势被 Flutter 页面拦截
这个问题在 PlatformView 和 Flutter 组件混排时太经典了。地图页面外层往往还有抽屉、底部弹窗等 Flutter 控件,用户在地图上滑动时,手势容易被外层 Flutter 识别成页面切换手势,导致地图拖动不畅。排查思路是检查手势竞技场的胜出方:如果你没有给地图容器设置透明点击穿透策略,Flutter 的GestureDetector会和原生地图抢事件。
我的解决方式是给地图容器上方的覆盖层设置明确的IgnorePointer,让地图区域的手势直接交给 PlatformView 处理。只有底部弹窗和顶部的悬浮按钮区域才响应 Flutter 手势。如果用的是PlatformViewLink,还要留意视图焦点和触摸拦截参数的配置,不同分支实现的差异比较大,建议在真机上逐个尝试,模拟器很难复现这类问题。
5.2 首次启动偏好设置默认值闪烁
没有正确处理异步加载的 App,第一次冷启动时设置页会先闪一下默认值,再变成用户真实存的值。这是因为偏好加载是异步的,界面第一帧渲染时内存里可能还没有数据。我前面说的“启动时一次性读入内存”就是为解决这个问题:在应用启动阶段先等待PreferenceStore.load()完成,再渲染主界面。虽然会多一个短暂启动等待,但换来了整个 App 生命周期内设置项同步读取的稳定体验。
如果你不想阻塞启动流程,也可以做一个 Splash 页来缓冲。我实测下来,井盖地图这种轻量应用在启动时读取几条 Preferences 的耗时基本可以忽略,不必为了炫技引入复杂方案。
5.3 大量 Marker 刷新导致掉帧
项目里井盖数量增长到几百个以后,全量刷新地图 Marker 出现过一次肉眼可见的卡顿。后来我发现卡顿不只在 Flutter 侧,原生侧批量添加 Marker 时如果逐条解析 JSON,同样会拖慢主线程。解决办法是两个方向一起做:Dart 侧把经纬度数组单独压缩成更精简的列表结构,原生侧尽量使用批量解析而不是循环创建单点 Marker。
如果地图数据量大到上万,前端一个点一个点画肯定不现实。这种情况建议在原生侧开启地图聚合能力,只把当前可视范围内的聚合点返回给 Flutter 展示。井盖管理虽然有资产数,但一个城市的可视区域同时展示几百个已经足够;真正的海量数据一般只在后台处理,不需要压在地图 SDK 上。
5.4 原生 Preferences 异步写回丢失问题
OpenHarmony 的 Preferences 写入后需要调用 flush 才能真正落盘,如果只调 put 不调 flush,应用被系统杀掉时数据很可能丢失。我在测试时遇到过一种情况:快速切换多个开关后立即杀进程,再重启发现只有最后一个设置生效,就是因为前面几次写入没有可靠落盘。
后来我在 Dart 侧做了一层节流:偏好变更一秒钟内多次触发时,只保留最后一次完整状态做持久化。例如用户快速连续打开三个开关,界面实时刷新,但写盘操作合并成一次。这样既能保存中间状态,又能减少 IO 次数,另外一定要在原生侧每个写入操作后面调用 flush 方法。
5.5 问题速查表
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 地图区域滑动卡顿或无响应 | PlatformView 手势与 Flutter 手势冲突 | 调整触摸拦截策略,让地图区域事件直通原生 |
| 设置页只显示默认值 | 异步加载未完成时渲染首帧 | 启动时预加载偏好,再构建主界面 |
| Marker 数量增加后明显掉帧 | 全量替换 Marker 且缺少聚合 | 批量刷新,开启地图聚合能力 |
| 快速修改设置后重启丢失 | 只 put 未 flush | 每次修改后调用 flush,Dart 侧做节流 |
| 地图主题和界面主题不同步 | 没有把偏好传给原生侧切换底图 | 监听偏好变化,同时调用原生地图风格切换 |
| 上报页面提交后闪退或数据缺失 | 页面在异步保存完成前被销毁 | 串行执行保存和导航返回 |
还有一个小提醒:Markdown 表格里能解决的问题别写进代码里。很多新手一碰到 Bug 就想去“加个判断”,其实先看设备日志定位到原生层还是 Dart 层才是正路。Flutter for OpenHarmony 的调试日志在混合输出时比较乱,建议开着 logcat 级别的过滤,只看与 Flutter 引擎和当前应用包名相关的输出。
最后分享一个我自己的体会:跑这种 OpenHarmony 上的 Flutter 项目,一开始不要指望所有插件都能开箱即用,也不要一上来就追求复杂架构。先耐住性子完成“Flutter UI + 原生桥接 + 偏好存储 + 地图刷新”这个小闭环,项目后续的价值才会慢慢显现。尤其是偏好设置,它看起来只是几个开关和一次存储,实际上是把整个 App 的“用户状态”串起来的最好抓手;把这块理清楚了,后面做通知推送、账号体系、多端同步都会顺很多。