先把背景交代清楚。上个月我们团队接了个市政井盖数字化管理的项目,需求很直白:把城区的几万个井盖位置从一堆Excel表格里倒腾到地图上,巡检终端用的是 OpenHarmony 系统的国产板卡,还要求后续能批量导入台账数据。团队里没人搞过鸿蒙原生开发,但大家都有 Flutter 项目经验,于是就有了这个 flutter_for_openharmony 城市井盖地图 App 的实战项目。这篇博文就把整个过程中最核心的批量导入实现、地图渲染、状态管理和打包上架踩过的坑完整复盘一遍,给同样准备入坑 OpenHarmony 的 Flutter 开发做个参考。
说实话,这个项目如果按传统思路去做,得养一个两到三人的 ArkTS 原生开发团队,工期至少翻倍。用 Flutter 复用的价值在于,业务层代码(井盖列表、详情页、筛选逻辑、数据模型)基本不用动,只需要把平台相关的地图、文件选择、数据库通过插件层适配到 OpenHarmony。文章后面会有完整的环境搭建、数据模型设计、批量导入链路、地图聚合渲染和真机打包的实操记录,想直接抄作业的可以跳过背景直接看后面章节。
1. 这个项目解决的真实问题:井盖台账为什么数字化这么急迫
1.1 井盖巡检业务的真实痛点
城市井盖管理的本质是“人找位置”。巡检员一天要处理几十个工单,很多时候不是井盖坏了很难修,而是根本不知道井盖在哪、归谁管、上次巡检是什么时候。数据散落在 Excel、纸质表格、甚至微信群接龙里,领导要个统计报表能折腾一整天。
我们的应用要解决三件事:第一,把井盖的物理位置落到地图上(经纬度 + 地址描述);第二,把井盖的权属、类型、状态这些属性结构化(谁负责、什么井、坏没坏);第三,也是这次实战的重点,把历史台账这批“离线数据”高效地批量导入到应用里,而不是让管理员对着手机一个个手动录入——几万个井盖手工录入,录到明年都录不完。
1.2 Flutter 和 OpenHarmony 的匹配度为什么高
OpenHarmony 开源之后,国产终端设备越来越多,但应用生态依然稀缺。很多业务团队跟我们的处境一样:有现成的 Flutter 代码,想低成本迁移到 OpenHarmony 设备上。Flutter 跑在 OpenHarmony 上并不是玄学,原理是 Flutter 引擎自绘渲染 UI,OpenHarmony 提供 ArkUI 容器作为承载,底层通过 PlatformView 和通道机制跟系统能力交互。对业务层来说,Dart 代码兼容性非常好。
具体到本项目的平台选型,我这边用到的链路是:Dart 业务层 -> Flutter 引擎自绘 -> OpenHarmony 平台适配层 -> 原生地图 SDK 和系统数据库。很多人问“openharmony os 是用什么语言编写的”,这个问题得分三层看:内核和底层是 C/C++,系统框架层是 C++ 加 ArkTS,应用层除了 ArkTS 之外也能跑 Flutter(Dart)、JS 等跨端框架。所以 Flutter 不是替代 ArkTS,而是以独立应用框架的形式运行在 OpenHarmony 的容器里。
1.3 地图 SDK 选型:为什么最后选了高德 OpenHarmony 版
地图是井盖应用的核心载体,选型不能拍脑袋。我对比了三个方案:华为 MapKit、高德地图 OpenHarmony 版、腾讯位置服务。华为 MapKit 在 OpenHarmony 上的适配最好,毕竟是自家生态,但是 POI 数据和周边搜索能力相对薄弱。高德 OpenHarmony 版的地图数据和 Android 版一致,接口也完整,而且对 PlatformView 的集成支持较好,开发过程中遇到的问题最少。腾讯位置服务官方适配文档少,社区案例也不多,属于备选。
选型还有一个现实因素:井盖巡检工单里经常要回答“离这个井盖最近的市政养护队在哪”,这个场景对 POI 搜索依赖很高,高德在这块的数据积累最扎实。最后我们就在 Flutter 侧桥接了高德的 OpenHarmony 原生 SDK,通过 PlatformView 方式嵌入地图组件。
地图切换前后的渲染性能也要提前说清楚:OpenHarmony 上 Flutter 使用 Skia 自绘,地图组件本身是原生绘制,重叠在 PlatformView 上,标记点数据量在几千个以内性能没问题,但如果不做聚合,上万标记点会让 UI 线程卡顿到掉帧,后面会专门讲聚合实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建复盘:Flutter 在 OpenHarmony 上从零跑通
2.1 工具链清单和版本选择
先把环境整明白。官方支持 OpenHarmony 的 Flutter 分支是从 Gitee 维护的 flutter_flutter 仓拉下来的,不能直接用 pub.dev 上的 Flutter 稳定版。我这边实际用的版本是 Flutter 3.7.12 对应的 OpenHarmony 分支,搭配 DevEco Studio 4.0 + OpenHarmony SDK API 10。
需要的工具清单:
- DevEco Studio 4.0 以上(用来构建 hap 包和真机调试)
- OpenHarmony SDK(API 10 或更高,DevEco 里直接下载)
- flutter_flutter 的 OpenHarmony 分支(gitee.com/openharmony-flutter/flutter_flutter)
- 高德地图 OpenHarmony 版 SDK(从高德开放平台下载)
2.2 初始化工程的关键配置
环境变量是第一个坑。如果你电脑上已经装了 Android 的 Flutter SDK,PATH 里会有两个 flutter 命令,不处理的话 flutter create --platforms=ohos 会直接忽略 OpenHarmony 平台。我处理的办法是单独开一个终端,把 OpenHarmony 分支的 bin 目录追加到 PATH 最前面。
bash复制git clone -b OpenHarmony-3.7.12-RELEASE https://gitee.com/openharmony-flutter/flutter_flutter.git
export PATH=$PWD/flutter_flutter/bin:$PATH
flutter config --enable-openharmony
flutter create --platforms=ohos city_cover_app
创建完成后工程目录里会多出一个 ohos 文件夹,里面就是 ArkTS 壳工程,跟 Android 的 android 目录、iOS 的 ios 目录并列。之后写 Dart 业务代码完全不受影响,只有涉及平台能力时才需要打开 ohos 目录改原生代码。
2.3 国内镜像源与 Gradle 下载超时
这个坑几乎每个人都会踩。OpenHarmony 的构建链路里,Gradle 负责把壳工程打包成 hap,而 Gradle 默认拉取依赖是从 Maven Central 和 Google 仓库走的,国内网络环境经常卡在“Building”阶段一动不动。解决办法是在 ohos 工程的 build.gradle 里把仓库替换成阿里云和腾讯云镜像。
groovy复制allprojects {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
mavenCentral()
}
}
Gradle 本身还要额外配置 gradle-wrapper.properties 的 distributionUrl,同样替换成腾讯镜像的 Gradle 发行包,否则第一个 flutter build hap 就能耗掉你半天。
2.4 双 Flutter 版本并存的注意事项
刚才提到 PATH 冲突,这里再补充一个细节:OpenHarmony 分支的 Flutter 和官方版的 Flutter 不能混用。如果你在 Android Studio 里打开过这个项目,AS 自带的 Flutter 插件可能会在后台执行 flutter pub get,这时候用的是官方版 Flutter,它不认识 ohos 目录,会把 pubspec.lock 搞乱。我后来养成的习惯是:OpenHarmony 工程一律用命令行操作 Flutter 命令,IDE 只用来写代码和看日志。
3. 井盖数据建模与本地存储:给批量导入留好“唯一性”底线
3.1 井盖实体字段怎么设计才不返工
井盖不是一个简单的点,它身上挂了一堆业务属性。我设计的实体包含这些字段:井盖编号(用于业务唯一标识)、名称、类型(雨水、污水、电力、燃气、通信)、状态(正常、破损、沉降、缺失、未确权)、权属单位、经纬度、地址描述、备注、导入批次号、创建时间戳、更新时间戳。
这里有两个字段是专门为批量导入设计的,新手容易忽略:
batch_no导入批次号:每次导入生成一个批次 ID,如果导入后发现数据有严重问题,可以按批次一键删除整批数据,不用逐条去清理。code + lat + lng组合唯一性:Excel 里可能同一个井盖被重复登记多次,经纬度相同但编号不同,去重逻辑必须同时考虑编号和地理位置。
3.2 为什么选 sqflite_ohos 而不是原生 RDB
OpenHarmony 自带的 relationalStore 功能上完全够用,但直接在 Flutter 里用需要自己写 MethodChannel 桥接,代码量大而且容易出错。我们用了 sqflite_ohos,这是 sqflite 在 OpenHarmony 平台的适配版,API 和 Android 端的 sqflite 完全一致,团队零学习成本,数据库文件也会自动放在应用沙箱目录,不需要额外处理存储权限。
数据库建表 SQL 长这样:
sql复制CREATE TABLE IF NOT EXISTS manhole_cover (
id TEXT PRIMARY KEY,
code TEXT NOT NULL,
name TEXT,
type TEXT,
status TEXT DEFAULT 'normal',
owner TEXT,
lat REAL NOT NULL,
lng REAL NOT NULL,
address TEXT,
remark TEXT,
batch_no TEXT,
created_at INTEGER,
updated_at INTEGER
);
CREATE UNIQUE INDEX idx_mhc_code ON manhole_cover(code);
CREATE INDEX idx_mhc_status ON manhole_cover(status);
CREATE INDEX idx_mhc_geo ON manhole_cover(lat, lng);
唯一索引是批量导入的底线,即使业务层去重逻辑出了 bug,数据库也能挡住重复数据。状态和经纬度的普通索引是给后续列表筛选和地图范围查询用的,几万条数据下没有索引,查询会慢一个数量级。
3.3 数据访问层封装思路
实际开发中千万不要在 UI 层直接写 SQL,井盖数据后面一定会扩展巡检记录、工单、照片等业务,数据访问层提前拆出来能少走很多弯路。我封装了一个 ManholeCoverDao,提供 insertBatch、queryByStatus、queryByRect(按经纬度范围查矩形区域)、deleteByBatchNo(按批次删除)等接口。批量插入必须在单事务里执行,可以保证要么整批成功、要么整批回滚,不会出现入了 500 条然后断了,剩下 500 条下落不明的尴尬情况。
4. 批量导入核心链路:Excel 解析、去重、分批入库与错误孤岛
4.1 文件选择和沙箱路径处理
OpenHarmony 和 Android 在文件选择上的权限模型不一样。Android 可以直接用 SAF 或者 FileProvider,OpenHarmony 则需要通过 DocumentViewPicker 拉起系统文件选择器,拿到的是一个 uri,不能直接在 Flutter 侧读取。我的做法是:在 ohos 原生侧封装一个 MethodChannel 方法,拉起 DocumentViewPicker 后,把选中的文件复制到应用沙箱的 cache 目录,再返回沙箱路径给 Dart 层。这个方案的稳定性比直接用 flutter 的 file_picker 插件要好得多,file_picker 当时在 OpenHarmony 上的支持还不够成熟。
选择文件后要限制格式和大小。井盖台账一般是 .xlsx,偶尔有 .xls,我的处理是:.xlsx 用 excel 包解析,.xls 提示用户先转成 xlsx 再导入——excel 包不支持老格式,硬解会乱码。文件大小限制在 10MB,超过的让用户按街道/社区拆分成多个文件分批导入,这样也顺便解决了超大数据量内存暴涨的问题。
4.2 Excel 解析不能放在主线程
excel 包是纯 Dart 实现,解析本身不慢,但井盖台账动辄几千上万行,解析过程中内存分配非常剧烈。如果直接在 UI 线程跑,界面会卡死好几秒,用户以为应用挂了。
解决方式是丢到后台 isolate 里跑:
dart复制final bytes = await File(path).readAsBytes();
final result = await Isolate.run(() {
final excel = Excel.decodeBytes(bytes);
// 遍历所有 sheet,解析行数据
return parsedItems;
});
这里一个容易被忽略的细节是:Dart 的 Isolate.run 不能直接传复杂对象,所以解析完的数据模型必须是纯 Map 或者 List<Map>,不要在 isolate 里创建 DAO 或数据库连接对象。
4.3 三层数据校验,缺一不可
批量导入不能“闭眼入库”,脏数据进去之后,地图上出现一堆坐标在海外的“幽灵井盖”,排查起来比手工录入还痛苦。我做了三层校验:
第一层是行结构校验:列数是否匹配模板、必填项是否为空、经纬度是否能转成 double 并落在合理范围(国内经纬度大致是经度 73~135、纬度 18~53)。
第二层是业务枚举校验:类型必须在 5 个枚举值里,状态必须在 5 个枚举值里,权属单位不能为空。
第三层是重复性校验:在内存里用 HashSet 保存 code_lat_lng 组合作为 key,同批导入中重复的数据直接标记为错误行,不进数据库。
dart复制final seen = HashSet<String>();
for (var row in rows) {
final key = '${row['code']}_${row['lat']}_${row['lng']}';
if (!seen.add(key)) {
errors.add(ImportError(rowNo: rowNo, reason: '重复井盖'));
continue;
}
// 其他校验...
}
4.4 分批事务:100 条一批是实测最优解
校验通过的数据进入入库阶段。不能一条一条 insert,也不能十万条一次性一个事务。前者事务提交太频繁,后者单条失败回滚整个库,风险太大。我实测下来每批 100 条是比较舒服的数字。
dart复制await db.transaction((txn) async {
for (var i = 0; i < items.length; i += 100) {
final end = (i + 100 > items.length) ? items.length : i + 100;
final batch = items.sublist(i, end);
for (var item in batch) {
await txn.insert('manhole_cover', item.toDbMap());
}
}
});
有人问为什么不直接用 SQLite 的批量插入语法 INSERT INTO ... VALUES (...), (...),这里要解释一下:sqflite 的 rawInsert 不支持多组 VALUES 参数化绑定,而且我们后续还要对每条失败数据做精确到行号的错误定位,逐条插入反而能拿到具体哪一行报了什么错。实测数据:1000 条井盖(每条约 20 个字段),预校验 + 去重 + 事务入库,总耗时约 2 秒;1 万条约 10 秒。这个性能在我们的市政终端上完全够用。
4.5 进度反馈、取消机制和错误文件导出
导入过程不是“转圈等结果”,用户需要知道到底导到哪了。我用 ImportProgressProvider 维护导入状态:当前处理到第几行、总行数、成功条数、失败条数,UI 上显示进度条和百分比。
dart复制class ImportProgressProvider extends ChangeNotifier {
int total = 0;
int current = 0;
int success = 0;
List<ImportError> errors = [];
bool isRunning = false;
bool cancelFlag = false;
}
每处理一批就调用一次 notifyListeners(),进度条就会实时滚动。取消逻辑是设一个 cancelFlag,每处理完一批检查一次,如果用户点了取消就抛异常回滚事务,已经写入的数据不残留。
导入完成后,如果存在错误行,应用会在沙箱的 Document 目录生成一个 import_errors.csv,包含原始 Excel 行号、井盖编号、错误原因三列。用户把这个文件拷出来,在源 Excel 里照着改完,下次再导入就行了。这个闭环设计在真实使用中非常受欢迎,避免了“你导入失败了,但为什么失败我完全不知道”的糟糕体验。
5. 井盖地图渲染:标记点聚合、状态着色与跨端事件通道
5.1 通过 PlatformView 嵌入高德 OpenHarmony 版地图
地图 SDK 接入这块,官方是有示例的,但要在 Flutter 里用需要包装一层 PlatformView。我在 ohos 目录里写了个 MapView 组件,实现 PlatformView 接口,并在 Flutter 侧用 UiKitView(平台视图)加载。
dart复制Widget build(BuildContext context) {
return UiKitView(
viewType: 'com.citycover.map',
creationParams: {'initialLat': 30.5, 'initialLng': 114.3},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: _onPlatformViewCreated,
);
}
创建好视图之后,通过 MethodChannel 跟原生地图通信:
dart复制static const platform = MethodChannel('city_cover_map');
原生侧监听同一个 channel,处理 setCenter、addMarkers、clearMarkers、setZoom 等方法。这个设计保证了 Android 和 OpenHarmony 两端共用同一套 Dart 层调用接口,未来如果想支持其他地图 SDK,只需要替换原生实现,Dart 业务代码一行都不用动。
5.2 大批量标记点必须做聚合,否则必卡
井盖数量过万之后,把所有 marker 一次性丢到地图上,即使高德原生 SDK 能扛住,PlatformView 的合成开销也会把 Flutter UI 线程拖垮。聚合是必选项。
高德 OpenHarmony 版自带的聚合功能当时还不够完善,所以我直接在 Dart 层写了一个网格聚合算法。思路很简单:根据当前缩放级别,把经纬度映射到一个网格 cell 里,同一个 cell 内超过一个点就合并成聚合 marker,显示“该区域有 N 个井盖”。
dart复制List<ClusterItem> buildClusters(List<ManholeCover> covers, int zoom) {
final gridSize = _gridSizeForZoom(zoom); // 不同缩放级别用不同网格大小
final map = <String, List<ManholeCover>>{};
for (final cover in covers) {
final key = '${(cover.lat * gridSize).round()}_${(cover.lng * gridSize).round()}';
map.putIfAbsent(key, () => []).add(cover);
}
return map.entries.map((e) {
if (e.value.length == 1) return ClusterItem.single(e.value.first);
return ClusterItem.group(e.value);
}).toList();
}
地图缩放时通过 MethodChannel 把新的 zoom 值抛回 Dart 层,重新计算聚合结果,然后一次性 updateMarkers。聚合逻辑放在 Dart 层的另一个好处是:可以通过单测直接验证聚合正确性,不用在真机上反复调试。
5.3 状态着色与自定义 marker 图标
井盖状态要能在地图上一眼区分,我用颜色做语义化:正常绿色、破损橙色、沉降黄色、缺失红色、未确权灰色。图标不用让原生侧去加载资源文件,而是在 Dart 侧绘制好图片字节(drawImage 或者直接用 Flutter 的 CustomPainter 生成 PNG),通过通道传给原生 SDK 作为 marker 图标。这样图标风格完全由 Flutter 统一控制,原生侧只负责显示。
dart复制final bytes = await createMarkerIcon(status: cover.status);
platform.invokeMethod('updateMarker', {
'code': cover.code,
'lat': cover.lat,
'lng': cover.lng,
'iconBytes': bytes,
});
5.4 点击井盖弹卡片与详情联动
地图点击事件通过 EventChannel 从原生侧回调到 Flutter:原生 SDK 检测到 marker 点击后,把该 marker 携带的井盖 code 通过事件通道抛给 Dart 层。Dart 层拿着 code 去 CoverMapProvider 里查实体数据,然后弹出底部卡片显示井盖基本信息、状态、权属单位,并提供“查看详情”按钮跳转到完整详情页。
事件通道的关键在于 val 和 symbol,原生侧发送的是 Map<String, Object>,Dart 侧监听后用 event.receiveBroadcastStream() 接收。这个通道是异步单向的,适合原生到 Flutter 的回调,跟 MethodChannel 的双向请求/响应用法区分开。
6. Provider 链路:跨组件状态共享在井盖场景的具体用法
6.1 为什么是 Provider 而不是 setState 或 Bloc
这个项目里,筛选条件、导入进度、当前选中的井盖、地图标记数据,分布在列表页、地图页、详情页和导入弹窗多个独立界面。如果用 setState,跨组件传参能把代码写成一团乱麻;用 Bloc 对这种中小项目又显得过重。Provider 是官方推荐的轻量级状态管理方案,基于 InheritedWidget 加 ChangeNotifier,学习成本低,逻辑直观。
入口处用 MultiProvider 统一注册:
dart复制void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ImportProgressProvider()),
ChangeNotifierProvider(create: (_) => FilterProvider()),
ChangeNotifierProvider(create: (_) => CoverMapProvider()),
],
child: const CityCoverApp(),
),
);
}
6.2 导入进度状态必须在主线程更新
这里有个容易踩的坑:批量导入的解析和入库逻辑都跑在 isolate 或者后台线程,最终更新 ImportProgressProvider 的操作必须在主 isolate 执行。Dart 的 UI 线程之外调用 notifyListeners(),轻则状态不刷新,重则直接崩溃。
我的处理方案是:isolate 解析完成后把结果通过 SendPort 回到主 isolate,入库过程使用数据库操作的异步回调本身就在主 isolate 完成,每处理完一批就更新 provider 里的计数器,然后 notifyListeners() 通知进度条刷新。这样逻辑上清晰,运行也稳定。
6.3 筛选状态与地图数据的联动时序
筛选功能是井盖应用的高频操作:按状态(只看破损)、按类型(只看电力井)、按权属单位。FilterProvider 保存这三个条件,列表页通过 Consumer<FilterProvider> 监听变化并重新查询数据库;地图页监听同样的 provider,筛选条件变化后重新从数据库按条件取点位、重算聚合、update markers。
这里要特别注意一个性能问题:不能每次筛选变化都销毁重建 PlatformView,那样地图会闪白且耗时严重。正确做法是保留地图视图,只更新 marker 数据。我封装的 CoverMapProvider.refreshMarkers() 会先 clearMarkers 再 addMarkers,在几千个点的规模下刷新时间在几百毫秒内,用户感知不到卡顿。
另外提一下 flutter 组件通信,在这个项目里有三个层面的通信方式:应用级共享状态用 Provider;Flutter 调原生能力用 MethodChannel;原生异步回调用 EventChannel。三者的分工非常明确,别混着用,否则调试时你会被各种回调绕晕。
7. 打包上架与真机排错:hap 签名、权限申请和几个典型运行时报错
7.1 hap 构建的两种方式
真机运行需要先构建 hap 包。DevEco Studio 里可以直接 Build > Build Hap(s)/APP(s) 生成,但对做 CI/CD 流水线的团队,命令行方式更友好:
bash复制hvigorw assembleHap --mode module -p product=default
构建产物在 ohos/entry/build/default/outputs/default/ 目录下。注意默认产出的 hap 是未签名的,不签名装不上真机,模拟器可以暂时跳过签名,但一旦涉及地图 SDK、定位权限,模拟器上的表现会和真机有较大差异,建议有条件直接真机调试。
7.2 签名流程:比 Android 复杂但流程固定
OpenHarmony 应用签名需要在 AGC(AppGallery Connect)后台创建应用,申请调试证书,生成 .p12 私钥库、.cer 证书和 .p7b profile 文件,然后在 DevEco Studio 的 Project Structure > Signing Configs 里配置。这个流程第一次走会觉得很繁琐,但后边都是重复操作。
签名有个特别容易踩的坑:高德地图的 API Key 是绑定包名和签名指纹的。如果签名换了,地图会白屏,且日志里只提示“appkey 验证失败”,不会告诉你具体原因。我在联调阶段就因为换了调试证书,整整排查了半天。所以务必在一开始就固定签名配置,不要频繁更换。
7.3 权限申请:LOCATION 是 user_grant 类型
module.json5 里声明权限:
json复制{
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{
"name": "ohos.permission.LOCATION",
"reason": "用于在地图上显示当前巡检位置",
"usedScene": {
"ability": ["EntryAbility"],
"when": "inuse"
}
}
]
}
INTERNET 属于 system_grant,安装即授权;LOCATION 属于 user_grant,必须在运行时通过 abilityAccessCtrl 发起弹窗授权请求。如果只声明不调用请求接口,定位功能会静默失效,地图倒是能显示,但当前定位点永远是空的。
7.4 真机调试复盘:flutter run 不支持的时候怎么办
按照惯常思路,肯定先 flutter run -d <device>,但 OpenHarmony 分支对 flutter run 的支持程度取决于版本。我遇到的情况是:直接 flutter run 会报 device 未找到,需要先构建 hap,用 DevEco Studio 安装到真机,再执行 flutter attach 附加调试。
热词里有很多人问“flutter 新建项目后跑不起来”,我复盘一下最容易出现的报错:
E/flutter ... [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception:多数是 so 库没加载完整,原因可能是 hap 未签名,或者资源合并时有残留文件。处理办法是flutter clean之后重新构建,问题能解决大多数。appkey 验证失败:地图白屏,检查包名和签名指纹是否跟高德后台一致。Unable to load script:Flutter 的 assets 没打进 hap,检查ohos工程配置里flutter_assets是否被正确打进资源目录,重新构建不要增量。
真机设备如果系统版本太低,地图 SDK 也可能初始化失败,我们统一要求设备 OpenHarmony 4.0(API 10)以上,保证 SDK 兼容性。
7.5 应用分发与合规
企业级 OpenHarmony 设备(市政终端、政务平板)通常不走公网应用商店,而是通过设备提供方的定制框架侧载安装,或者统一推送。发布前需要准备应用签名认证、备案材料、以及政务项目要求的各类安全说明。这块不是技术代码能绕过的,项目排期时要提前留出材料准备时间,否则应用都打包好了,却卡在安装环节上不了真机。
最后再分享一个落地经验:批量导入这个功能做完后,记得把导入模板(Excel 表头 + 示例数据)也一起放到应用里提供下载。实际上用户拿到手之后,大多会直接拿自己的 excel 改,但如果模板不规范,导入错误率会非常高。我们后来在模板里加了数据校验提示列、枚举值下拉框,导入失败率从初期的 30% 降到了 5% 以下。这个细节看着不起眼,却是让功能真正“可用”的关键一步。
