首页 / 资讯中心 / 文章详情

OpenHarmony Flutter实战:井盖地图与工单闭环开发解析

OpenHarmony Flutter实战:井盖地图与工单闭环开发解析 ★ FEATURED ARTICLE
1. 项目概述与整体思路接到这个项目的时候我第一反应是城市井盖地图App这不就是那种“看起来简单、做起来全是坑”的典型项目吗实际动手之后发现远比预想的复杂。表面上是把井盖点位画到地图上实际上要把“地图展示、空间查询、工单流转、图片取证、离线缓存”全部捏合到一个运行在OpenHarmony设备上的Flutter应用里。这个标题里的“实战”两个字我认为重点就在工单详情那部分因为地图点位做出来只是第一步工单详情才是真正串联业务的地方。项目背景是很典型的市政管养场景。某城市治理平台需要对辖区内的井盖设施做全生命周期管理外勤巡查人员拿着终端设备在App上查看当前区域的井盖分布点开某个井盖就能看到它的基本信息、权属单位、历史维修记录。更重要的是平台可以把维修任务直接派发给外勤人员外勤人员查看工单详情按照流程到现场处理然后拍照、填写处理说明、上报结果。整套流程里地图是定位入口工单是本。App需要同时满足两个角色的需求一是巡查人员快速发现问题和上报问题二是维修人员接收工单并完成闭环处理。为什么选择Flutter for OpenHarmony因为项目要覆盖的设备形态比较杂。OpenHarmony生态里有传统手机形态的终端也有带三防外壳的工业手持设备甚至有一些嵌入式的巡检终端设备厂家不同、屏幕分辨率不同如果每个终端都用ArkTS原生开发一套业务开发和维护成本都扛不住。Flutter的跨端UI方案可以把页面层、状态层、业务逻辑层全部复用到各形态设备上只需要在平台层针对不同设备差异做适配。这个方案还有个额外好处Dart侧代码未来如果哪天要迁移到Android、iOS或桌面端也能比较平滑地复用大部分代码不至于被某个系统綁死。技术栈确定之后我按“平台适配层、业务服务层、UI展示层”三层来划分工程结构。平台适配层负责OpenHarmony和Flutter的桥接包括定位权限、相册权限、拍照、文件存储、原生地图能力封装业务服务层负责井盖数据模型、工单状态机、网络请求、本地数据库缓存、图片压缩上传UI展示层承载地图页、列表页、工单详情页、登录页等页面。这个分层思路在后面的开发和排错中帮了大忙至少我们排查问题的时候能快速定位到底层是平台侧问题还是业务逻辑问题还是UI渲染问题。2. 开发环境与工程搭建2.1 OpenHarmony侧的SDK与Flutter版本对齐OpenHarmony上的Flutter开发和Android开发有个非常大的不同Flutter官方对OpenHarmony的支持走的是社区适配路线而不是官方主线。所以第一步不是急着写代码而是把SDK和Flutter版本对齐。我这里用的版本组合是OpenHarmony SDK API 9以上的某稳定版本配合社区维护的Flutter for OpenHarmony分支。为什么强调版本对齐因为OpenHarmony的API演进比较快而Flutter的鸿蒙适配分支往往只支持某个特定的SDK API等级如果OpenHarmony SDK版本太新或者Flutter分支太旧编译的时候会出现各种让人抓狂的符号找不到、接口签名不匹配的问题。建议的做法是搭建工程时先跑通一个最小Demo再开始写业务代码。工程目录结构类似manhole_project/ ├── ohos/ # OpenHarmony 原生工程目录 ├── lib/ # Flutter Dart 代码目录 ├── pubspec.yaml └── build script这里有个经验不要直接用命令行的默认模板生成工程最好从某个已经验证过的开源样例工程开始改。因为OpenHarmony工程配置项比Android工程多一些比如module.json5里的abilities配置、权限声明方式、aar/har依赖的引用路径如果从零手写容易漏配置。从样例工程改造至少保证基础编译链路是通的。2.2 Dart侧依赖与鸿蒙原生包的关系Flutter工程需要用到三方插件时在Android生态里是Gradle自动拉取aar但在OpenHarmony上逻辑不太一样。鸿蒙侧是hvigor构建依赖的native能力通常是har包或直接源码方式引入的Dart侧的插件必须要有对应的鸿蒙实现否则编译能过运行时报“MissingPluginException”。我踩过的一个很典型的坑是Dart侧用了某个通用的路径处理插件Android上是正常工作的但是在OpenHarmony设备上运行到某个功能时报MissingPluginException。排查后发现这个插件的鸿蒙适配实现只实现了部分接口路径处理相关的method没有在鸿蒙侧注册。解决办法有两个要么换一个已经完整适配鸿蒙的插件要么自己在ohos目录下写一个MethodChannel实现。所以项目的依赖管理要特别注意每个依赖需要确认是否在OpenHarmony上有完整实现不能只看pub.dev上写了支持Flutter就觉得可以。像这个项目里用到的图片选择、定位、权限请求这几个关键插件我最后都选择了自研或深度定制而不是直接引第三方原因就是避免插件适配不全导致上线后出问题。2.3 平台通道封装定位、权限与相册平台通道是OpenHarmony和Flutter通信的桥梁。Android是用MethodChannel鸿蒙侧实现方式类似只是原生代码语言是ArkTS。我们封装的几个核心通道需要重点说明一下。定位通道Flutter侧调用Dart接口获取当前坐标鸿蒙侧通过系统定位服务实现。这个通道必须设计成异步回调形式因为定位本身是耗时操作而且首次定位可能需要几秒甚至十几秒。我在Dart侧定义了如下接口class LocationService { static const _channel MethodChannel(manhole/location); static FutureLocationResult getCurrentLocation() async { final result await _channel.invokeMethod(getCurrentLocation); // 解析result返回经纬度、精度、时间戳 } }鸿蒙侧对应的实现需要先在module.json5里声明ohos.permission.LOCATION权限然后在定位服务代码里注册位置回调拿到坐标后通过result.success返回给Dart侧。这里提醒大家OpenHarmony的权限模型是运行时动态授权的不是安装时就授予的所以Dart侧调用前要好权限状态检测鸿蒙侧做一个权限请求的承载。相册和相机通道的逻辑类似但要注意的是OpenHarmony的图片选择器和Android不同它返回的媒体URI不是本地文件路径直接拿这个URI去存储或上传是会出问题的。需要在鸿蒙侧先把URI转换成可读的文件路径或者直接返回字节流再从Dart侧处理。我们这边的方案是鸿蒙侧用一个FileAsset实体把path、name、size、mimeType都传到Dart这样Flutter侧做压缩和上传就省事多了。3. 井盖地图模块实现3.1 地图选型自绘地图图层地图是整个系统的视觉核心。本来想接入某个商用地图SDK的鸿蒙适配版本但调研后发现我们的需求其实并不需要完整的地图引擎而是要展示井盖点、支持点击交互、支持缩放平移。商用SDK在这个项目里有点“杀鸡用牛刀”的感觉而且还可能面临SDK授权和鸿蒙适配风险。最终我选择了自绘图层方案使用一个可以拖动缩放的画布自行绘制底图网格和井盖点位在此基础上做交互。自绘地图的好处非常多。第一不依赖第三方地图服务的SDK适配不会因为某个地图SDK不支持OpenHarmony而受阻第二数据完全在我们手里可以做到离线可用——把一定范围内的井盖数据缓存到本地即使网络不好也能看到点位。第三性能好控制我们只需要绘制点、线、简单图形帧率比完整地图引擎更高。缺点也很明显没有道路底图、没有实时路况、没有POI检索。但对这个项目来说这些都不是刚需。巡查人员关心的是“井盖在哪个位置、怎么走、谁负责”这类信息通过坐标加上简单的行政区划底图就能支撑起来。自绘地图实现的核心是坐标变换把经纬度坐标映射到画布像素坐标。我用了一个简单的墨卡托投影近似。Offset worldToOffset(LatLng latlng) { double x (latlng.lng 180) / 360.0 * _worldSize; double y (1 - _log(tan(pi / 4 _radians(latlng.lat) / 2)) / pi) / 2 * _worldSize; return Offset(x - _viewCenter.dx _canvasSize.width / 2, y - _viewCenter.dy _canvasSize.height / 2); }LatLng offsetToLatLng(Offset offset) { double x offset.dx - _canvasSize.width / 2 _viewCenter.dx; double y offset.dy - _canvasSize.height / 2 _viewCenter.dy; double lng x / _worldSize * 360.0 - 180.0; double lat _degrees(atan(sinh((1 - y / _worldSize * 2) * pi))); return LatLng(lat, lng); }缩放级别通过_worldSize来控制_worldSize越大显示的坐标范围越小地图越“放大”。这个方案的实际渲染效果是够的至少在我们测试的几台OpenHarmony设备上交互流畅度完全可以接受。3.2 井盖数据模型与点聚合地图上要展示的井盖点位数据来自后端接口包含经纬度、类型、状态、权属单位等信息。单条数据不复杂复杂的是数量。市域范围内井盖可能有几万甚至十几万个如果全部一次性拉到本地且直接绘制不管是网络、内存还是渲染层都会吃不消。所以要做两个处理数据预加载和点聚合。数据预加载策略是按地图可视区域动态请求。当地图中心点变化或者缩放级别变化时根据当前可视范围的经纬度边界向后端请求这个范围内的井盖数据。为了防止频繁请求我加了一个去抖机制只有地图停止移动超过300毫秒才发起新的请求。数据返回后缓存在内存Map里如果再次滑动到相同区域直接从内存读取不再重复请求。点聚合的逻辑要处理的是“同一像素范围内多个点重叠”的情况。当缩放级别小、可视范围大时附近的几个井盖在屏幕上可能挤到同一个像素附近如果不做聚合并渲染用户根本点不准。聚合算法不算复杂我实现了一个基于网格的聚合把屏幕划分成64x64像素的网格每个网格内的井盖聚合成一个聚合点显示数量点击聚合点则以该点为中心放大一级地图。MapString, ListManholeCover aggregatePoints(ListManholeCover points) { final grid String, ListManholeCover{}; for (final point in points) { final offset worldToOffset(point.latLng); final key ${(offset.dx ~/ 64)}_${(offset.dy ~/ 64)}; grid.putIfAbsent(key, () []).add(point); } return grid; }聚合点绘制成带有数字的圆形标记普通点绘制成井盖类型对应的小图标。这样分层计算结果也简化了点击命中检测先判断是否命中聚合点再判断是否命中单个井盖点。3.3 图层交互与点击命中检测地图页面的交互主要有三种拖动平移、双指缩放、点击点位。自绘方案里拖动和缩放用GestureDetector处理点击集用命中测试来处理。有个很关键的性能细节不要在build方法里做大量的hit test遍历。井盖数据可能有上千个点如果每次点击都循环遍历所有点判断是否命中UI线程会明显卡顿。我的做法是先把点的像素坐标提前算好并缓存存储成一个空间索引结构。点击时先根据点击坐标做网格索引查找只检查命中网格内的点这样每次点击最多比较十来个点速度很快。final spatialIndex String, ListManholeCover{}; void rebuildSpatialIndex(ListManholeCover points) { spatialIndex.clear(); for (final point in points) { final offset worldToOffset(point.latLng); final key ${(offset.dx / 64).floor()}_${(offset.dy / 64).floor()}; spatialIndex.putIfAbsent(key, () []).add(point); } } ManholeCover? hitTestPoint(Offset position) { final key ${(position.dx / 64).floor()}_${(position.dy / 64).floor()}; final candidates spatialIndex[key]; if (candidates null) return null; for (final point in candidates) { final offset worldToOffset(point.latLng); if ((offset - position).distance 24) return point; } return null; }还有一个坐标系的细节大家要留意地图数据源提供的经纬度可能是WGS-84坐标系但国内地图服务通常使用的是GCJ-02或更高精度的坐标系。如果不做转换点位会偏几百米甚至几公里。这个项目里井盖坐标是从政务平台下发的基础台账里来的源头往往是测绘部门测的WGS-84坐标展示在我们自绘的地图上不做偏转问题不大但如果后续要对接第三方地图做路径规划就必须先做WGS-84转GCJ-02的纠偏。项目里我预留了纠偏函数接口方便后续扩展。3.4 定位与当前点位展示打开地图后的默认行为是定位到用户当前所在位置并显示周围的井盖。这里涉及两个问题定位权限申请流程和定位精度处理。权限申请流程在OpenHarmony上有自己的AB模型和Android的权限弹窗机制略有不同。鸿蒙侧需要在你请求定位服务前先检查权限状态如果没有授权要调用requestPermissionsFromUser来拉起授权弹窗。由于权限弹窗必须在用户事件回调里拉起不能异步随机弹所以Dart侧的定位按钮点击后要先通过MethodChannel通知鸿蒙侧去申请权限等授权完成再发起定位。定位精度处理方面OpenHarmony系统定位返回的坐标可能有几十米到几百米的误差尤其是室内定位误差更大。我在地图上做了一个精度圈指示从定位服务拿到accuracy字段后在地图上画一个以定位点为中心、半径为accuracy的圆让用户直观看到定位误差范围。同时结合混凝土井盖通常分布在路侧的特征在地图中用候选点吸附算法把定位点吸附到最近的可达道路上的井盖旁边提升体验。LatLng snapToNearbyManhole(LatLng current, ListManholeCover points) { ManholeCover nearest; double minDistance double.infinity; for (final point in points) { final d distanceInMeters(current, point.latLng); if (d minDistance) { minDistance d; nearest point; } } if (minDistance 200) return nearest.latLng; return current; }这里的“吸附”不是把定位点硬拽到井盖上而是如果附近200米内确实有井盖把中心点引导到井盖旁方便用户优先看到待巡检目标。如果周围没有井盖还是显示原始定位结果。4. 工单详情功能实现4.1 工单信息模型与状态流转工单详情是整个App里业务含量最重的模块也是最需要用心设计的部分。我先从数据模型说起。工单的字段很多核心几类基本信息工单编号、标题、紧急程度、下发时间、位置信息井盖ID、坐标点、地址描述、处理信息当前状态、当前处理人、处理时限、上报节点、到达时间、完成时间、附件信息现场照片、语音备注、流程记录每一步状态变更的操作人、时间、意见。这些字段不仅详情页要用列表页过滤、统计报表也要用所以数据模型要尽量接近后端结构。工单状态我是用枚举来定义的状态流转严格按照业务规则来enum WorkOrderStatus { created, // 创建 dispatched, // 已派发 accepted, // 已接单 processing, // 处理中 rejected, // 已驳回 completed, // 已完成 }状态之间的转移规则created只能到dispatcheddispatched只能到accepted或rejectedaccepted只能到processingprocessing只能到completed。这个规则我在前端和服务端都做了一遍校验前端能提前拦截掉不合法的操作后端能保证最终一致性。详情页底部的操作按钮就是根据当前状态动态渲染的已派发状态显示“接单”和“退回”按钮处理中状态显示“提交完成”按钮已完成状态不显示任何操作按钮。4.2 详情页布局拆解工单详情页的UI我分成了五个区域从上到下依次是工单状态区、基础信息区、井盖定位区、附件区、流程记录区。工单状态区是整个页面的视觉锚点用一张横跨页面宽度的状态卡展示当前工单的状态名称、状态颜色、紧急程度标识顶部还有一个处理倒计时。倒计时的计算要特别注意不能用服务器返回的deadline减去当前时间的简单逻辑因为可能存在时区差异。我的做法是后端同时下发deadlineTimestamp和serverTime前端计算差值而不是依赖本地时钟的绝对值。基础信息区展示工单编号、派发单位、当前处理人、联系电话、地址描述等用表格形式排列。这里有个交互细节联系电话字段不应只是一段文本而是要支持点击直接拨号因为外勤人员在现场经常需要跟调度中心确认情况。电话拨号在鸿蒙侧要单独封装一个能力通过MethodChannel调用。井盖定位区在详情页里嵌入一个地图预览图不是完整的地图操作视图就是一张包含目标井盖点位的静态快照。点这个预览图可以跳到地图页做完整展示并自动发起导航路径规划。这块的实现是把井盖经纬度传给地图页的参数对象地图页收到参数后自动定位到该井盖、弹出详情气泡。我一开始图省事想把地图预览直接用完整地图组件做缩小版结果发现每次进详情页都要重建地图组件开销太大、卡顿明显后来改成静态快照方案流畅度立竿见影。附件区展示现场照片的缩略图网格点击可以查看大图长按可以删除。附件区还支持拍照新增和从相册选择新增。每个图片都需要显示上传状态待上传、上传中、已上传、失败。上传状态的实时反馈很关键因为外勤现场网络经常不稳定用户拍完照片后如果不能立刻看到上传状态会非常焦虑也容易重复提交。流程记录区是一个纵向时间线展示工单从创建到当前的所有操作节点。每个节点显示操作人、操作时间、操作动作和备注。这个区域的数据来自后端的flowHistory接口前端只做展示。有一个值得分享的细节时间线末端的圆点要根据状态动态变化颜色已完成节点用蓝色填充当前节点用圆环样式这样用户的视觉重心能快速落脚在“进度到哪里了”。4.3 图片拍摄、压缩与上传策略现场拍照取证是井盖维修工单里最重要的环节图片质量和上传效率之间必须找到平衡。原始的拍照图片在OpenHarmony手持设备上通常每张在3MB到8MB之间如果工单要传6张图就是几十MB的数据在弱网环境下根本传不动。必须做压缩。压缩策略我踩了几次坑后才稳定下来。第一不能只看设置quality因为不同图片的原始尺寸不一样单纯压缩质量可能压不掉大图。我的方案是先限制最长边不超过1600像素再设置质量系数0.7同时设定单张图片体积上限300KB如果压缩后还是超过300KB进一步降分辨率到1280并降质量到0.5。FutureFile compressAndLimit({required String filePath, required int maxBytes}) async { var imageFile File(filePath); var imageSize imageFile.lengthSync(); var decodeWidth 1600; var qualityRate 70; while (imageSize maxBytes (decodeWidth 600 || qualityRate 30)) { final tempDir await getApplicationDocumentsDirectory(); final outputPath ${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg; final compressed await FlutterImageCompress.compressWithImageFile( imageFile, minWidth: decodeWidth, minHeight: decodeWidth, quality: qualityRate, format: CompressFormat.jpeg, outputPath: outputPath, ); imageFile File(outputPath); imageSize imageFile.lengthSync(); if (decodeWidth 600) decodeWidth - 200; qualityRate - 10; } return imageFile; }上传策略采用串行队列加自动重试。为什么串行不是并行因为井盖维修现场往往网络不好多个图片并发上传会抢带宽导致每张都传不上去串行上传至少能保证前面的图片尽快传输完成。每次上传前生成一个taskId上传成功后标记图片状态为已完成上传失败则进入重试队列用户回到页面时可以一键重试。为了防止用户在某张图片上传失败后无感知我在图片缩略图的右上角加了一个红色感叹号标识点击后弹出“重新上传”操作。4.4 流转记录时间线实现时间线组件项目里很多页面都要用我单独抽成了TimeLineView组件。实现方式不复杂核心是竖线和圆点的绘制用ListView承载列表项每个列表项的左侧通过Container加圆点、右侧放置内容、下方用一条竖线连接到下一个节点。最后一个节点不显示竖线。class TimeLineView extends StatelessWidget { final ListFlowRecord records; Widget buildItem(BuildContext context, int index) { final record records[index]; final isLast index records.length - 1; return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 左侧时间轴列圆点 竖线 SizedBox( width: 48, child: Column( children: [ Container(width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: index records.length - 1 ? Colors.blue : Colors.grey, )), if (!isLast) Expanded(child: Container(width: 2, color: Colors.grey[300])), ], ), ), // 右侧内容区 Expanded(child: Padding( padding: const EdgeInsets.only(bottom: 16, right: 12, top: 4), child: Column(...), )), ], ), ); } }不过这里有个坑Row的crossAxisAlignment如果用默认值竖线的高度会比较尴尬需要用IntrinsicHeight来让Row撑满内容高度。或者更省事的做法不用弹性布局直接给每条记录设置一个基于内容预估的固定高度但那样如果文本换行长度不同看起来会不整齐。我最终用了IntrinsicHeight方案实测在几十条记录的时间线下性能没有问题。时间线数据里如果操作人离职了或者账号注销了怎么办这个问题我在项目里遇到过。后端返回的opName字段可能存在null前端要处理这个情况显示“未知操作人”不能直接渲染null导致崩溃。4.5 离线暂存与补偿提交工单处理有一个真实场景外勤人员到了井盖位置发现现场信号质量特别差可能连得上看地图但传不了图片。这种情况下如果严格在线提交工单根本完不成。我做了离线暂存功能方案是维护一个本地待提交队列。用户在详情页填写完处理说明、拍完照片后点击“提交完成”按钮如果检测到网络不可用先不提示错误而是把工单ID、处理说明、图片文件本地路径、提交时间存储到本地数据库的pending_submit表里同时把图片文件保留在应用沙箱目录UI上显示“已保存待网络恢复后自动提交”然后通过一个轮询机制每两分钟检测一次网络状态网络恢复后自动把暂存的数据按顺序提交上去并同步状态。class OfflineSubmitManager { Futurevoid submitPending() async { final pendingList await LocalDb.queryPendingSubmits(); for (final item in pendingList) { final connected await NetworkProbe.isConnected(); if (!connected) break; final success await SubmitApi.submitWorkOrder(item); if (success) { await LocalDb.markPendingSubmitted(item.id); } } } }这里要注意图片文件的处理。图片压缩后我存储的是一个绝对路径但这个路径在应用重启后不一定仍然有效尤其是如果之前用的是临时目录。我的建议是压缩后就把图片复制到应用文档目录下一个专门的管理文件夹路径存数据库提交成功后根据配置决定是否删除。不能提交成功就立刻删因为还要考虑服务端可能异步处理失败回滚稳妥的办法是保留一天后再自动清理。5. 状态管理、路由与数据持久化5.1 状态管理选型Riverpod还是Provider项目进入中期后页面间的共享状态越来越多比如地图页选中的井盖工单列表页的筛选条件全局的用户登录态和当前定位。最开始我直接用Provider包了几个全局Provider后来发现地图页的井盖数据每次数据更新都会触发地图重绘重绘还要等点位计算完成导致卡顿最后决定迁移到Riverpod。迁移的原因是Riverpod可以在不重建整个Widget树的情况下精细控制监听范围。比如地图数据Provider更新时我只让绑定了点位集合的Widget重新渲染而不是整个地图页。Riverpod的Selector语法可以精确选取需要监听的字段这个在复杂页面里的价值非常大。final manholeDataProvider FutureProvider.autoDisposeListManholeCover((ref) async { final visibleBounds ref.watch(mapBoundsProvider); return await ManholeApi.fetchManholes(visibleBounds); }); // 在Widget里只监听需要的字段 final manholes ref.watch(manholeDataProvider.select( (data) data.asData?.value ?? const [], ));实际使用中我发现Riverpod的autoDispose在有地图这种场景要特别小心。当地图短暂滑出可视区域再回到原区域时autoDispose可能已经把Provider销毁了重新创建后会再次请求接口造成重复加载。所以地图数据相关的Provider我改成了普通Provider不自动销毁而是用缓存时间控制刷新频率。5.2 网络请求层与错误处理网络层其实是最能体现一个App“耐不耐用”的地方。整体架构是Dio封装统一的ApiClient所有请求增加超时控制、统一的返回码处理、Token刷新机制和错误码映射。请求设计的几个细节值得分享。第一时间超时不能一刀切。普通查询接口超时设置10秒图片上传接口的超时设置60秒因为图片在弱网环境下确实要更久的等待时间。第二返回码要统一封装后端接口的返回报文可能是{code:0, data:{}, message:}这种格式但不同接口可能code表示含义不同我在ApiClient层统一处理业务层拿到的已经是解析好的对象不用每个页面都判断一遍。关于Token失效常见做法是全局拦截401响应后自动刷新Token刷新成功则重放原请求。这个机制在OpenHarmony的Flutter应用里同样适用。有一点要注意如果多个请求同时返回401会触发多个刷新Token并发请求需要加锁避免重复刷新导致死锁。我的方案是维护一个Future来完成刷新操作所有401请求都等待同一个Future。弱网环境下请求失败会走重试逻辑。我针对不同接口设置了不同的重试次数查询类接口重试2次提交类接口使用了前面提到的离线队列不直接重试而是先落库避免反复请求把用户流量耗光。5.3 本地数据库与缓存同步这类应用对数据离线能力的要求比较高涉及缓存的数据有三大类基础字典数据井盖类型、单位列表、地图点位数据、工单数据。基础字典数据更新频率极低适合全量缓存地图点位数据体量大、更新频率中等适合按区域增量缓存工单数据变化频繁本地只保留最近三十天的已完成的工单记录避免本地数据库无限膨胀。本地存储我选择了Sqflite的鸿蒙适配版本。为什么不用Hive因为工单数据有比较多的按字段筛选和组合条件查询的需求Hive这种KV数据库做起来不太顺手。Sqflite在鸿蒙上的适配已经比较成熟SQLite的数据库文件也能在系统之间自由迁移。地图点位数据的缓存策略稍微特殊一点。为了减少本地数据库体积我不把整条井盖记录都存成JSON而是单独建了一张bin表把经纬度、类型、地图上需要展示的字段存为结构化字段把权属单位、地址、历史维修记录等字段存成JSON字符串查询时再按需解析。这样地图渲染时不需要解析大JSON性能更好进入详情页时才去解析JSON获取完整信息。6. 常见问题排查与性能优化实录6.1 常见问题速查表我把这个项目过程中遇到的一线问题整理成了一个速查表各位在OpenHarmony上跑Flutter应用时很可能也会碰到。问题现象排查思路解决方案Flutter页面初始化加载慢检查引擎启动是否在主线程是否有大量同步IO使用FlutterEngine预热延迟创建非首屏页面地图拖动卡顿检查每帧是否做命中测试、是否同步IO点位索引缓存、命中测试分帧延迟处理图片上传失败且无回调检查网络状态判断逻辑确认是否走了离线队列网络探测使用真实请求不依赖系统广播MethodChannel调用无响应检查鸿蒙侧是否注册了对应方法方法名大小写统一方法名命名规范两侧代码同步核对字体异常或中文显示为方块检查OpenHarmony系统字体加载路径工程内嵌入所需字体文件FontLoader加载状态更新后UI不刷新检查Provider使用的是不是同一个容器使用UncontrolledProviderScope并统一入口表格里有几个问题值得展开。字体问题是最容易忽略的OpenHarmony设备的中文字体文件路径与Android不同如果三方字体库加载失败整个页面会显示成方块乱码非常影响演示效果。我的做法是将常用字体文件打包进应用资源通过Dart侧FontLoader主动注册不依赖系统字体。还有一个高频问题OpenHarmony应用退到后台再回来Flutter页面动画卡顿或黑屏。这类问题多半是系统窗口SYSTEM_UI的焦点没有及时交给Flutter视图导致。处理方式是在页面onShow事件中调用FlutterView重新获取焦点。这个修复有点偏原生但影响很大建议大家把它作为通用适配步骤写进项目模板。6.2 渲染卡顿与降频优化地图页的渲染优化值得单独写一节因为井盖点位多的时候如果优化不得当帧率会跌到可以感知的程度。优化工作分两部分绘制阶段和构建阶段。绘制阶段点位的数量直接影响Canvas的drawPath调用次数。我们点位数在1000以下时普通绘制没有压力但到了3000以上就要做分帧和范围过滤。范围过滤的思路很直接只绘制在当前屏幕可视区域内的点。点位坐标部分虽然可能在屏幕外但是聚合点、标注文字、气泡控件仍然会被构建。所以地图页我把标注文本做成了延迟加载缩放到一定级别以上才显示文字标签低级别只显示点。这个策略能让绘制指令减少一半以上。构建阶段的优化更重要。地图页的主体是CustomPaint如果井盖点位的Paint对象每次重绘都重建开销很大。我把所有静态的Paint对象提取为final字段只在地图缩放级别变化时更新。同时把每次重绘的索引结构缓存起来不是每次刷帧都重建索引。这几个改动加起来在测试机上地图滑动帧率从30帧以下提升到接近60帧。还有一个很隐蔽的问题要提醒当井盖数量多的时候地图页面的widget树层级不要设计得过深。一个常见错误是地图页外层套了多个Padding和Container每次手势事件都要穿透多层手势识别器事件响应会明显变慢。地图页我优化到从根节点到CustomPaint只有三层Widget手势识别直接放在最顶层。6.3 OpenHarmony包签名与发布注意事项OpenHarmony应用上线和Android发布流程差异较大这里简单说一下关键步骤。首先是签名配置需要在工程配置中设置签名证书证书的生成和配置通过OpenHarmony的签名工具完成。不同设备如果调试运行还需要配置设备的调试证书。其次OpenHarmony应用在真机调试时需要通过工具连接设备并开启设备的开发者模式这和Android的adb逻辑类似但命令行工具和配置路径不一样。打包产物是HAP包。如果项目需要上架到某个应用市场还需要关注目标设备的API等级兼容性。OpenHarmony版本迭代快不同设备出厂系统版本可能差异很大兼容性测试要覆盖API 9和API 10两个等级。我就在这个上面吃过亏开发时用的是API 10的特性结果有一批部署设备是API 9编译能过运行直接报NoSuchMethodError。所以大家如果在项目里用了某个API的特殊能力一定要到目标设备上回归验证不能只看模拟器。发布前还有一项必做工作混淆配置。OpenHarmony应用默认不做代码混淆如果对安全性有要求需要在构建配置中开启混淆并配置keep规则把工单相关model类保留否则会影响反射和序列化。这个点文档里写得很浅实践起来细节不少建议时间充裕的话就先用默认配置确保功能稳定后再做混淆优化。写在最后的一点实操体会这个项目做下来我最深的体会是Flutter for OpenHarmony并不是一套“装好就能跑”的开发体验它更像是在一个还在快速演进的平台上做定制开发每个环节都要更谨慎一点。如果非要说哪些经验最值钱我总结三条。第一先把最小闭环跑通再铺开功能。不要在OpenHarmony工程配置还一头雾水的时候就开始写业务页面先跑通一个带着定位权限、地图自绘、工单提交的空壳应用等于把整个链路里最不确定的地雷排掉后面的开发都会变得很顺畅。第二一定要把平台通道的接口定义当成API来对待。Dart侧和ArkTS侧的方法名、参数类型、回调时序必须文档化谁改谁维护。项目后期我们踩的MissingPluginException类问题有一半是因为两侧代码不同步导致的。第三工单详情这类业务页面一定要先梳理状态机再写界面。我一开始先画UI结果写到“驳回重做”和“撤回待审”这几个状态时才发现界面上的操作逻辑根本对不上返工了一次。先把状态流转和操作规则敲定页面代码其实只是规则的翻译器。项目后续如果要扩展我建议可以加一个“常用位置收藏”功能让外勤人员可以收藏自己负责的井盖集合每次打开地图直接聚焦到自己的管养区域这个需求在真实业务里呼声很高。数据维度上可以考虑对接井盖的物联传感器数据让地图上的井盖不仅能看静态台账还能实时展示水位、倾斜角度等动态信息。不过这些都是后话了先把地图和工单闭环做扎实这个App在真实场景里的价值就已经不小了。
阅读完成 · 觉得有帮助?
咨询建站