第一次接到 what3words 鸿蒙化适配需求时,我第一反应是:这玩意儿不是有官方 Flutter 插件吗?直到打开鸿蒙构建日志,看到一屏 “Unhandled Exception: MissingPluginException” 才明白,问题没那么简单。what3words 的方案很有意思:把整个世界切成无数个 3 米见方的格子,每个格子用三个英语单词当地址。比如 “///filled.count.soap” 就是伦敦某处的一个点,比经纬度好记,比门牌号精确。但鸿蒙生态起步晚,官方 Flutter 插件根本没适配鸿蒙,你只能自己动手给这套核心能力做一次“鸿蒙化手术”。
这篇文章不打算写那种“一步步点击”的入门教程,而是把我在真实项目中踩过的坑、验证过的路径、最终落地的方案全盘托出。内容适合两类人:一类是在鸿蒙上做 Flutter 应用、想集成 what3words 的开发者;另一类是以后要适配其他三方库到鸿蒙,想搞懂“鸿蒙化到底在化什么”的人。我会从编码原理讲到平台通道实现,再讲到坐标资产在业务里的玩法,最后把那些报错和崩溃的排查过程也一起交代清楚。
1. 认识 what3words 与坐标转换原理
1.1 三词网格:3 米格子背后不是魔法,是算法
what3words 的核心是一套全局网格划分体系。地球表面被划成约 57 万亿个 3 米 × 3 米的方块,每个方块拥有一个由三个单词组成的唯一标识。这三个单词不是随机拼凑的,它遵循一套固定的编码逻辑:先把经纬度通过等距投影转换成平面坐标,然后在网格坐标系里取整,再通过一套混淆算法将整数映射到三个词列表索引上。反过来,拿到三个词也能通过逆映射还原出网格中心的经纬度。
这里有个很关键的细节:单词表不是英文全量词典,而是经过筛选的、发音清晰、拼写简单、语义无歧义的约 4 万词表。每个词对应一个数值,三个词组成一个类似“base-40000”的三位数系统。为什么用三个词而不是两个?因为 40000 的三次方是 640 亿,还覆盖不了 57 万亿格子,所以实际词表长度和编码范围经过了严格匹配。也正因如此,你几乎不会遇到发音相近导致翻车的情况。
从开发者视角看,你不需要实现这套算法。what3words 提供了 HTTP API 和若干语言 SDK,把“词转坐标”和“坐标转词”变成了两个简单的网络请求。但这恰恰是鸿蒙适配的第一个痛点:官方 Flutter 插件内部依赖了 what3words 原生 Android/iOS SDK,而鸿蒙上既没有这两个 SDK 的运行时,也不能直接调用它们。所以我们要做的不是重写算法,而是搭一座从 Flutter 到鸿蒙的桥,桥那头接 what3words 官方 REST API,桥这头维持 Flutter 原有的调用方式。
1.2 鸿蒙适配的真实难点:不是加密壳,是桥梁
在鸿蒙设备上跑 Flutter 应用,本身已经需要特殊的 Flutter 鸿蒙分支(OpenHarmony 适配版)。这个分支支持了 Dart 层和鸿蒙平台层之间的 MethodChannel / EventChannel,但很多插件仍然没有鸿蒙原生实现。what3words 官方 Flutter 插件就是典型:它在 pub 里最新版依然只有 Android/iOS 的 Platform 实现,在鸿蒙上初始化就直接抛 MissingPluginException 或者干脆找不到符号。
你可能会想:那我在 Dart 层直接换成 HTTP 请求不就行了?可以,但 Flutter 工程里已经写好了调用 what3words 插件的业务代码,比如What3words.instance.wordsToCoordinates(...)、CoordinatesToWords等,到处改调用逻辑工作量很大,而且后续如果要升级官方插件,你的私有改动会非常痛苦。
所以最优解是:在鸿蒙端自己实现一个同名插件,把官方插件的 Dart 接口原样“接住”,底层用鸿蒙的网络请求去请求 what3words API。这样业务代码不用动,只需在鸿蒙侧补上 Platform 实现。这个思路同样适合地图、支付、推送等任何“官方不支持鸿蒙”的 Flutter 插件。
1.3 方案选型:自研通道、HTTP 直连、社区桥接
我当时梳理出三条路,列个表格方便对比:
| 方案 | 接入成本 | 维护成本 | 风险点 |
|---|---|---|---|
| 修改官方 Flutter 插件,往里添鸿蒙实现 | 中(需改插件源码) | 高(每次升级都要合并) | 官方代码结构复杂,容易冲突 |
| 在 Flutter 工程里用 HTTP 替代插件全部调用 | 低(只改 Dart 层) | 中(业务代码侵入大) | 无法复用官方 SDK 的离线包、缓存等特性 |
| 自研鸿蒙 Platform 层,维持原有 Dart 接口 | 高(需搭通道、写原生) | 低(业务层零改动) | 需要自己实现网络层和错误处理 |
我最终选了第三条。原因很简单:这个项目的业务层有一两百处调用 what3words 的地方,改 Dart 层等于重写业务;而且我打算让鸿蒙端和 Android/iOS 端共用同一套 Dart 代码,未来随时可以把官方鸿蒙支持接回来。自研通道虽然前期累,但一劳永逸。
2. 鸿蒙化适配实操:从零封装一个 Flutter 插件
2.1 环境准备:你需要的不是鸿蒙 IDE,而是一条畅通的构建链
开始干活前,先把环境踩实。我用的组合是:DevEco Studio 5.0(鸿蒙应用开发)+ Flutter 3.19.0 的 OpenHarmony 分支(flutter_flutter),再加上 OpenHarmony SDK API 11。如果你拿到的是“鸿蒙版 Flutter”发行包,记得先跑flutter doctor确认openharmony出现在设备列表里。
有一个坑必须提醒:鸿蒙 Flutter 工程里,插件目录结构和 Android/iOS 不一样。Android 插件是android/src/main/kotlin,鸿蒙插件则是ohos/src/main/ets(Deveco 工程下的 ArkTS 代码)。你需要在pubspec.yaml里声明pluginClass和packageName,就像这样:
flutter: plugin: implements: some_what3words_plugin platforms: ohos: pluginClass: What3wordsPlugin package: com.example.w3w_ohos这段配置决定了 Flutter 引擎怎么在鸿蒙侧找到你的插件入口。别小看这几十行,我见过好几个项目就是忘了加implements导致插件根本无法加载。想要保险,你可以直接用 Flutter 官方提供的模板命令flutter create --template=plugin --platforms=ohos my_plugin,目前社区已经提供了鸿蒙模板支持,省得手写桥接目录。
2.2 ArkTS 侧写出 MethodChannel 服务:最核心的 200 行
接下来是重头戏:在鸿蒙端写一个 ArkTS 类,实现MethodChannel的回调。在ohos/src/main/ets/What3wordsPlugin.ets里,核心逻辑有三段:
第一段是注册通道。需要拿到 Flutter 引擎给的PluginUtils,调用getMethodChannel("what3words/coordinates", handler)。注意通道名称必须和 Dart 侧MethodChannel名字完全一致,否则 Dart 调用时找不到实现。
第二段是处理调用。MethodChannel 的onMethodCall会返回方法名和参数。我们需要实现两个方法:wordsToCoordinates和coordinatesToWords。前者入参是字符串三词地址,后者入参是一个包含lat和lng的 Map。为了保持接口兼容,返回值结构也要模仿官方插件:一个包含经纬度、坐标精度、三词地址的 Map。
第三段是网络请求。官方 what3words API 需要传入key(以后统称密钥),完整请求是https://api.what3words.com/v3/convert-to-coordinates?words=...&key=...。在鸿蒙里用@ohos.net.http模块发 GET 请求就行。这里有个坑:鸿蒙的 HTTP 模块回调是 Promise 风格,但 MethodChannel 的响应期望是同步返回值。所以你必须把异步封装成Promise再await,最后把结果传给result.success(...)。如果你直接同步 return,Flutter 端会收到null,然后大概率抛空指针。
代码骨架大概是这样的,我省略了错误分支,但你可以看到映射关系:
import http from '@ohos.net.http'; import MethodCall from '@ohos.application.methodCall'; export default class What3wordsPlugin { onMethodCall(call: MethodCall, result: any) { if (call.method === 'wordsToCoordinates') { this.wordsToCoordinates(call.arguments, result); } else if (call.method === 'coordinatesToWords') { this.coordinatesToWords(call.arguments, result); } } async wordsToCoordinates(args: any, result: any) { const request = http.createHttp(); const url = `https://api.what3words.com/v3/convert-to-coordinates?words=${args.words}&key=${args.key}`; request.request(url, (err, data) => { if (!err && data.result.responseCode === 200) { const body = JSON.parse(data.result.result as string); result.success({ latitude: body.coordinates.lat, longitude: body.coordinates.lng, words: args.words, nearestPlace: '' }); } else { result.error('W3W_ERROR', 'Request failed', err?.message || data.result.responseCode); } }); } }这里我故意把“坐标转词”省略了,因为代码结构几乎一样,只是请求/v3/convert-to-coordinates变成了/v3/convert-to-3wa,参数从words变成coordinates=lat,lng。建议你直接看官方 API 文档,把两个函数都补上,然后重点测试边界参数。
2.3 Dart 侧保持原接口:让业务层毫无感知
自研鸿蒙插件时,Dart 侧最好直接复用官方插件的接口定义。做法是修改 Flutter 工程的 pubspec,可能这样写:
dependencies: what3words: path: ./plugins/what3words_harmony然后在lib/what3words.dart里,暴露和官方一模一样的类名和方法名。我推荐直接抄官方插件的 dart 代码,只把底层的MethodChannel('what3words/coordinates')改成你自己的名字。这样业务层用await What3words.instance.wordsToCoordinates('filled.count.soap')时,根本不知道底层已经换成了鸿蒙原生请求。
这里有个细节得注意:MethodChannel 通信本身是异步的。如果你在 ArkTS 侧用Promise发送 HTTP 请求,Dart 侧拿到结果会有一个微任务延迟。实测在鸿蒙设备上,一次“词转坐标”从 Dart 发出到拿到结果大约耗时 280ms,比 Android 上的 150ms 慢了一些,但尚可接受。如果你需要频繁转换,提前在 Dart 层做缓存或节流。
2.4 完整链路演示:把一次三词查询从起点跑到终点
适配之后,链路是这样的:
- Flutter 业务层调用
What3words.instance.wordsToCoordinates('///filled.count.soap')。 - Dart 层生成
MethodCall('wordsToCoordinates', {'words': 'filled.count.soap', 'key': 'YOUR_API_KEY'})。 - 通过 StandardMethodCodec 编码后,经 Flutter 引擎发送到鸿蒙侧 MethodChannel。
- 鸿蒙侧
onMethodCall收到参数,用http.createHttp()请求 what3words 服务器。 - 服务器返回经纬度 JSON,鸿蒙侧解析成 Map,再通过
result.success()回传 Dart。 - Dart 侧把 Map 转成
What3wordsCoordinates对象,业务层直接使用。
这过程中最常出问题的是第 5 步:ArkTS 的http.request回调类型和 Dart 侧的Map结构如果不匹配,回传的时候就会类型转换失败。我建议在 ArkTS 侧统一返回一个扁平 Map,不要用嵌套coordinates.lat这种深层结构。官方插件的标准返回确实是嵌套的,但鸿蒙侧我们完全可以在 Dart 层做二次映射,这样原生侧代码更简单,也更不容易踩类型不一致的坑。
3. 坐标资产的落地实战:在鸿蒙应用中使用 what3words
3.1 资产化概念:三词地址不只是字符串,是业务数据
“坐标资产”这个词听起来玄,其实说白了:经纬度加三词地址,在很多场景下就是核心业务数据。比如物流公司每单的收货点、救援队要定位的遇险点、户外活动分享的营地位置。在鸿蒙应用里,我们要做的不是调一个接口这么简单,而是把三词地址纳入数据生命周期的管理:生成、存储、校验、展示、分享。
我的做法是封装一个CoordinateAsset模型,包含words、lat、lng、timestamp、source五个字段。这样既能把“坐标资产”当成对象在 Dart 层传参,也能在鸿蒙侧通过序列化存储到本地数据库。因为三词地址是自然语言的,它天然适合做日志展示、文本共享、甚至语音播报,而经纬度则适合做可视化。两者并存,才叫“坐标资产”。
3.2 场景一:物流收货码,让三词地址代替“第五个路灯右转”
很多海外物流项目已经在用 what3words 做“最后一公里”。用户在下单时填三词地址,司机端用三词转坐标,再跳到鸿蒙地图里导航。鸿蒙上我们实现了同一套接口,业务逻辑直接复用。唯一需要注意的是:物流场景里用户可能输入大小写不一致,what3words 官方要求使用小写并去掉///前缀,所以我们在 Dart 层统一做toLowerCase()和trim(),保证请求格式正确。
我在适配过程中还顺手加了一个功能:把三词地址中的空格自动转成英文句点。因为官方 API 接受的格式是filled.count.soap,但用户可能习惯在输入框里用空格分隔。这个转换必须在调用鸿蒙通道之前完成,不要指望原生层去处理。
3.3 场景二:户外救援与位置分享,离线兜底是生死线
救援场景里,手机可能没信号,API 调用必然失败。what3words 官方提供了离线 SDK,但是鸿蒙没有。所以我们做了一个本地缓存:把最近 1000 次查询的“三词 ↔ 坐标”对存进鸿蒙数据库,同时在应用启动时预置一份常用区域的离线映射表(比如每个城市选几万个热点三词地址)。
这样当网络不可用时,鸿蒙插件会先查本地缓存,命中就直接返回;没命中则返回一个W3W_OFFLINE_ERROR。Dart 层收到这个错误后,可以提示用户“当前位置暂无法识别三词地址,请尝试靠近已知地标”。虽然不能覆盖全部场景,但至少不至于在野外直接白屏。
别小看这个兜底设计,我在真机测试时把系统网络关掉,连续调用了 50 次坐标转换,有 12 次命中缓存,平均响应小于 10ms。剩下的 38 次虽然失败了,但错误码清晰,业务层可以走降级弹窗。这在户外救援工具里非常加分。
3.4 场景三:把三词结果映射到鸿蒙地图
拿到经纬度之后,下一步当然是画在地图上。鸿蒙官方地图组件(MapKit)已经支持MapView显示标记点,但如果你的 Flutter 工程里用的是高德或华为地图插件,要注意鸿蒙适配的差异。
最顺滑的方式是:在 Dart 层把经纬度传给鸿蒙端的地图组件,利用 UIAbility 的 XComponent 机制嵌入。我这里是直接用了一个开源的 Flutter 鸿蒙地图插件,然后在它的onMapReady回调里加入了 marker 展示逻辑。注意地图坐标系是 WGS84 还是 GCJ02,what3words 返回的坐标是 WGS84 经纬度,国内地图通常要转成 GCJ02。如果你海外用,可以直接忽略。如果国内用,这个火星坐标偏移一定要处理,否则你会看到标记点“漂移”到几个路口之外。
这里分享一个实操技巧:在三词转坐标成功后,立即把lat、lng传给地图 SDK,让地图先moveCamera到目标点,再添加 marker。如果反过来先添加 marker 再 moveCamera,部分国产地图组件会有 0.5 秒的白屏闪烁,体验不太好。
4. 常见坑位与排查实录
4.1 “Unhandled Exception: MissingPluginException” 要怎么根治
我在开头提到的e/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] unhand...这串日志,实际上就是 MissingPluginException 的底层输出。它出现的路径是:Dart 调用了 MethodChannel,但鸿蒙侧没找到对应的 plugin class。八成原因是pubspec.yaml里ohos平台的pluginClass写错,或者鸿蒙插件没有被flutter pub get正确编译进工程。
排查步骤我记成了口诀:一看插件是不是声明了 ohos 平台;二看鸿蒙侧的入口类有没有被注册到PluginManager;三看ohos目录下有没有ets/源码被打进 HAR 包。前两种问题相对好治,第三种比较隐蔽:鸿蒙插件编译出的 HAR 包如果没有把.ets文件带进去,运行时自然会找不到符号。这种情况建议直接flutter clean然后重新构建。
4.2 API 密钥别硬编码,鸿蒙端要防窥屏
what3words 的密钥分前端限制型和服务端型。如果你在 App 里调用 REST API,必须在 what3words 控制台申请“前端限制型”密钥,并设置允许调用的域名和包名。鸿蒙端没有包名校验,但你可以把密钥放到应用沙箱里,通过环境变量读取。更稳的做法是:让 Flutter 业务层从你自己的后端获取一个短期 token,再传给鸿蒙通道,避免把长期密钥躺在 App 里。
我在项目里做了一个安全策略:密钥一天轮换一次,Dart 层启动时先从配置中心拉取,然后缓存在内存里,不落盘。鸿蒙通道每次只接收当天有效的 key,过期就报 401,业务层去刷新。这样即使截了包,密钥有效期也很短。不过你得在自己后端控制 by 调用频率,防止被刷。
另外,what3words 的 API 是按调用次数计费的。如果用户在 App 里疯狂拖拽地图导致每秒请求几十次,你月底的账单会很好看。我加了一个 Dart 层节流器:同一三词地址 5 秒内重复请求直接返回缓存,不同地址 100ms 内最多并发 3 个。
4.3 离线转换的 4 个实战细节
离线转换是容易被忽略的坑。如果你有离线包,注意三件事:
第一,词表大小。what3words 官方离线包分区域,一个国家的包大约 100~300MB,下载到本地要控制缓存目录,别塞到内存路径。鸿蒙的沙箱路径可以通过context.filesDir获取,建议把离线包放在这里。
第二,离线包更新。what3words 偶尔会调整编码网格或词表(大概每年一次),你需要设计一个版本号机制。我用了一个简单方案:每次应用启动时检查离线包版本,与服务端比对,发现旧版就静默下载,下载完原子替换。千万不要边用边删旧文件,否则数据会损坏。
第三,离线精度。我实测过离线转出来的坐标和在线转的结果可以相差 3~5 米,这通常是网格边界取整误差。如果不影响业务(比如物流收件人自取),可以忽略;如果用于高精度定位(比如设备测绘),则必须在线校验。
4.4 性能优化:MethodChannel 别传大象
另一个体验问题来自 MethodChannel 本身。不要试图用通道传输大字符串或频繁调用。我一个同事试过把一张 Base64 图片从 Dart 传到鸿蒙侧去识别,结果耗时 3 秒多,直接卡掉 UI 线程。what3words 虽然传的是小字符串,但如果你批量查询 100 个三词地址,建议把任务切分,每批 10 个,或者全部在 Dart 层并发await,让鸿蒙侧自己管理并发。实测并发 10 个请求比串行快 4 倍,比单次传数组安全得多。
鸿蒙侧还要注意 HTTP 连接复用。我每次请求都http.createHttp()再销毁,导致 TCP 连接频繁建立,大概多耗 30% 时间。后来我把HttpRequest对象存成单例,复用同一个连接,性能提升明显。你可以参考@ohos.net.http的HttpRequest文档,设置connectTimeout和readTimeout,避免弱网下请求悬挂。
5. 安全与合规:三词地址的隐私红线
你可能觉得三词地址就是几个单词,和隐私无关。但别忘了,它本质上是一个高精度坐标点的编码。如果用户的位置是通过三词地址分享出去的,那么这三词地址就等于把经纬度暴露给了接收方。尤其在鸿蒙这种强调数据安全的平台上,我们必须谨慎处理。
我的经验是:不要在日志里打印完整的三词地址和完整坐标,因为一旦日志上传到第三方崩溃分析平台,用户位置就泄露了。可以只打印“前两个词加星号”这种脱敏版本。数据库本地存储时也要采用加密方案,鸿蒙提供了@ohos.security.huks加密接口,我建议用 HMAC 对用户的三词地址字段做摘要,同时搭配随机盐,这样即便数据库被拖库,也无法直接还原出精确位置。
另外,what3words 的 ToS 要求不得将其用于跟踪他人位置(除非获得明确授权)。在 App 里接入这个能力时,你需要主动设计一个“位置分享二次确认”弹窗,避免用户无意间把自己的三词地址发出去了。鸿蒙系统本身有应用权限管理,但那是属于系统级的定位权限,和三词地址 API 是两回事。合规的底线是:你没有在后台默默转换某个人的位置。
前面绕了一大圈,其实核心就一句话:鸿蒙化适配的本质不是“重写”,而是“桥接”。你不需要精通 what3words 的网格算法,也不需要深挖华为 API 的每一个细节,只要把 Flutter 和鸿蒙之间的通道打通,并且在边缘场景上做足防护,就能让原本只有 Android/iOS 一半的功能在鸿蒙上完整跑起来。
我在真实项目里被 MissingPluginException 折磨过一整晚,也在离线包版本升级时把线上地图搞白过一次。但等我把这套插件封装稳定之后,业务层再也没有改动过一行 what3words 调用代码。如果你想在自己的鸿蒙应用里集成类似能力,我的建议是:先画一张调用链图,明确哪些逻辑留在 Dart,哪些放到 ArkTS,哪些该走网络,哪些该落缓存。想清楚这些,再照着本文的骨架去写,速度会快很多,坑也会少很多。