maps_launcher 的鸿蒙适配教程
maps_launcher 的鸿蒙适配教程
本文配套仓库:https://atomgit.com/oh-flutter/maps_launcher(TAG:
3.0.0-ohos-1.0.0-beta.1,分支:feat/ohos_maps_launcher_3.0.0)。插件的接口用法、真机效果与业务侧最佳实践,见配套的《maps_launcher 的鸿蒙使用指南》;本文解决的是另一件事:从上游 GitHub 仓库开始,把 maps_launcher 完整适配到 OpenHarmony / HarmonyOS 平台,并在真机上验证。
maps_launcher 是 pub.dev 上的一个打开地图应用的 Flutter 插件(作者 pikaju,MIT 协议,3.0.0+1),支持按名称搜索地点和按坐标展示目的地。它原本支持 Android、iOS、桌面与 Web,唯独没有鸿蒙;而鸿蒙 Flutter 应用要打开地图,得自己写平台通道,再组装 ArkTS 侧的系统导航面板参数。本文完整走一遍社区三方库适配的标准流程:把上游仓库同步到 AtomGit,拉到宿主机,建适配分支,用命令自动补全 ohos 目录,补全 Dart 与 ArkTS 两侧实现,补齐适配说明文件后提交分支,最后用仓库自带的 example 在真机上验证。
一、环境搭建
鸿蒙 Flutter 开发环境(ohos 版 SDK、DevEco Studio、签名配置)的完整搭建步骤,官方指南已经写得很细,直接照做即可:
适配工作比单纯使用多一项要求:终端里 flutter 命令必须指向 ohos 版 SDK,因为后文自动补全 ohos 目录靠的是它提供的 flutter create --platforms ohos 能力。环境装好后用 flutter devices 确认能识别鸿蒙真机。本文实测使用的环境:
| 项 | 版本 |
|---|---|
| Flutter(ohos 版) | 3.41.10-ohos-1.0.1 |
| 编译 SDK | 6.1.0(23) |
| 实测真机 | HUAWEI nova 12 Ultra(ADA-AL10,HarmonyOS 6.1.0.135 / API 24) |
二、适配过程
2.1 将上游仓库同步到 AtomGit
鸿蒙 Flutter 三方库社区(oh-flutter 组织)托管在 AtomGit,上游项目在 GitHub,适配的第一步是把上游代码完整迁入 AtomGit 上为它新建的目标仓库 oh-flutter/maps_launcher。做法是把上游克隆下来,添加 AtomGit 远端后整库推送,保留全部 commit 历史与 TAG,后续上游发新版时也能用同样的方式增量同步:
# 克隆上游仓库,目录名与目标仓库保持一致
git clone https://github.com/pikaju/flutter-maps-launcher.git maps_launcher
cd maps_launcher
# 关联 AtomGit 目标仓库
git remote add atomgit https://atomgit.com/oh-flutter/maps_launcher.git
# 推送全部分支与 TAG
git push atomgit --all
git push atomgit --tags
推送完成后,打开 AtomGit 上目标仓库的页面,能看到与上游一致的提交历史和源码目录:

图一:同步完成后 AtomGit 目标仓库的代码页
2.2 拉取代码到宿主机
从目标仓库把代码拉到本地,后续所有操作都在这份代码上进行:
git clone https://atomgit.com/oh-flutter/maps_launcher.git
cd maps_launcher
此时的目录还是上游的原始结构,只有 Android、iOS 等平台,没有 ohos 目录:
maps_launcher/
├── android/ # Android 平台实现
├── ios/ # iOS 平台实现
├── linux/ # Linux 平台实现
├── lib/ # Dart 接口(maps_launcher.dart)
├── example/ # 上游自带示例工程
├── pubspec.yaml # 插件描述与平台注册
└── ...

图二:clone 完成后的仓库目录
2.3 创建适配分支并补全 ohos 目录结构
社区约定适配分支统一以 feat/ohos_库名称_版本号 命名,maps_launcher 适配的上游版本是 3.0.0+1,分支名就是 feat/ohos_maps_launcher_3.0.0。先建分支:
git checkout -b feat/ohos_maps_launcher_3.0.0
然后在插件根目录执行一条命令,自动完成 ohos 适配结构的补全:
flutter create --platforms ohos .
这条命令由 ohos 版 Flutter SDK 提供,它读取 pubspec.yaml 里的插件名,自动生成完整的 ohos/ 目录,并在 pubspec.yaml 的 plugin.platforms 下追加 ohos 注册节点:
flutter:
plugin:
platforms:
# ... android、ios 等原有节点保持不变
ohos:
package: com.example.maps_launcher
pluginClass: MapsLauncherPlugin
生成的 ohos/ 目录结构:
ohos/
├── index.ets # 插件导出入口
├── oh-package.json5 # ohpm 包配置(main 指向 index.ets)
├── build-profile.json5 # hvigor 构建配置
├── hvigorfile.ts # hvigor 构建脚本
├── BuildProfile.ets # 构建信息(自动生成)
└── src/main/
├── module.json5 # 模块配置(har 类型)
└── ets/components/plugin/
└── MapsLauncherPlugin.ets # 插件模板(空实现,待补全)
几个文件的角色需要分清:oh-package.json5 声明了对 @ohos/flutter_ohos 的依赖(Flutter 鸿蒙嵌入层),index.ets 负责把插件类导出给宿主工程,src/main/ets/components/plugin/MapsLauncherPlugin.ets 是 flutter 工具按包名生成的插件模板——此时里面只有空的生命周期方法,这就是 2.4 要补全的文件。

图三:命令执行输出与生成的 ohos 目录
2.4 在插件文件中补全 ohos 实现
适配遵循"只做加法"的原则:两个公开接口的签名与返回值语义不变,Android、iOS 等原有平台的实现路径一行不动,所有改动都是新增。改动集中在两个文件:
| 文件 | 改动 |
|---|---|
lib/maps_launcher.dart | 新增鸿蒙平台分支:检测到 ohos 时改走 MethodChannel,其余平台维持原逻辑 |
ohos/src/main/ets/components/plugin/MapsLauncherPlugin.ets | 把模板补全为完整实现:注册通道、组装参数、拉起系统导航面板 |
先说 Dart 侧为什么要动。 原库在非 Android、iOS 平台一律拼接 Google Maps 网址交给 url_launcher 打开浏览器;鸿蒙上如果不动 Dart,调用会被送进浏览器,永远到不了系统导航面板。而原库 Android、iOS 的原生插件代码只是模板,Dart 从未调用它们,所以鸿蒙需要的 MethodChannel 通路是全新的。在 lib/maps_launcher.dart 中新增三段代码,首先是通道与平台检测:
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// OpenHarmony/HarmonyOS: reuses the `maps_launcher` channel that upstream
/// already registers on Android/iOS (upstream native code is template-only
/// and never called from Dart). On ohos the plugin resolves the launch via
/// native `startAbilityByType('navigation')`.
static const MethodChannel _ohosChannel = MethodChannel('maps_launcher');
/// OpenHarmony/HarmonyOS detection. The OpenHarmony Flutter fork reports
/// `Platform.operatingSystem` as `'ohos'`.
static bool get _isOhos => !kIsWeb && Platform.operatingSystem == 'ohos';
鸿蒙版 Flutter 引擎在 dart:io 的 Platform.operatingSystem 里返回 'ohos',这是官方预留的平台标识,配合 kIsWeb 排除 Web 端。然后是两个公开接口的入口,各自在方法体最前面加一个分支:
static Future<bool> launchQuery(String query) async {
if (_isOhos) {
return _invokeOhos('launchQuery', <String, Object?>{'query': query});
}
return await launchUrl(
createQueryUri(query),
mode: await _launchMode(),
);
}
static Future<bool> launchCoordinates(double latitude, double longitude,
[String? label]) async {
if (_isOhos) {
return _invokeOhos('launchCoordinates', <String, Object?>{
'latitude': latitude,
'longitude': longitude,
if (label != null) 'label': label,
});
}
return await launchUrl(
createCoordinatesUri(latitude, longitude, label),
mode: await _launchMode(),
);
}
命中分支时参数以 Map 形式过通道,未命中时走原有 URL 路径——原有接口实现的语义没有被触碰。最后是通道调用的容错封装:
static Future<bool> _invokeOhos(
String method, Map<String, Object?> arguments) async {
try {
return await _ohosChannel.invokeMethod<bool>(method, arguments) ?? false;
} on PlatformException {
return false;
}
}
原生侧抛 PlatformException 时降级为 false,与 url_launcher 的失败语义对齐,调用方拿到的是统一的 Future<bool>,不需要为鸿蒙写额外的异常处理。
再看 ArkTS 侧。 打开模板文件 ohos/src/main/ets/components/plugin/MapsLauncherPlugin.ets,补全为以下完整实现:
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
AbilityAware,
AbilityPluginBinding,
StandardMethodCodec,
} from '@ohos/flutter_ohos';
import { common, UIAbility } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class MapsLauncherPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
private static readonly CHANNEL_NAME: string = 'maps_launcher';
private channel: MethodChannel | null = null;
private ability: UIAbility | null = null;
getUniqueClassName(): string {
return 'MapsLauncherPlugin';
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
MapsLauncherPlugin.CHANNEL_NAME,
StandardMethodCodec.INSTANCE
);
this.channel.setMethodCallHandler(this);
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.ability = binding.getAbility();
}
onDetachedFromAbility(): void {
this.ability = null;
}
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
switch (call.method) {
case 'launchQuery': {
const query: string | undefined = call.argument('query');
if (query == undefined) {
result.error('ERROR', 'launchQuery requires a "query" string argument', null);
return;
}
// 位置搜索场景(sceneType: 3):destinationName 为搜索关键字(必填)
const wantParam: Record<string, Object> = {
'sceneType': 3,
'destinationName': query
};
this.startNavigationAbility(wantParam, result);
break;
}
case 'launchCoordinates': {
const latitude: number | undefined = call.argument('latitude') as number | undefined;
const longitude: number | undefined = call.argument('longitude') as number | undefined;
if (latitude == undefined || longitude == undefined) {
result.error('ERROR', 'launchCoordinates requires "latitude" and "longitude" number arguments', null);
return;
}
const label: string | undefined = call.argument('label') as string | undefined;
// 地点详情场景(sceneType: 4):destinationName 可选(对应上游 label 参数)
const wantParam: Record<string, Object> = {
'sceneType': 4,
'destinationLatitude': latitude,
'destinationLongitude': longitude
};
if (label != undefined && label != '') {
wantParam['destinationName'] = label;
}
this.startNavigationAbility(wantParam, result);
break;
}
default:
result.notImplemented();
break;
}
} catch (err) {
// 与 url_launcher 语义对齐:失败返回 false,不向 Dart 抛异常
result.success(false);
}
}
private startNavigationAbility(wantParam: Record<string, Object>, result: MethodResult): void {
if (this.ability == null) {
result.error('ERROR', 'UIAbility is not attached', null);
return;
}
const context = this.ability.context as common.UIAbilityContext;
// MethodResult 只允许回复一次;startAbilityByType 的异步回调(onError/onResult)与
// 同步 AsyncCallback 可能先后触发,用标志位保证只回复一次
let replied: boolean = false;
const replyOnce = (value: boolean): void => {
if (!replied) {
replied = true;
result.success(value);
}
};
const abilityStartCallback: common.AbilityStartCallback = {
onError: (code: number, name: string, message: string) => {
replyOnce(false);
},
onResult: (abilityResult: common.AbilityResult) => {
replyOnce(true);
}
};
try {
context.startAbilityByType('navigation', wantParam, abilityStartCallback, (err: BusinessError) => {
if (err) {
replyOnce(false);
} else {
replyOnce(true);
}
});
} catch (err) {
replyOnce(false);
}
}
}
实现有四个关键点。
生命周期上,插件同时实现 FlutterPlugin、MethodCallHandler、AbilityAware 三个接口:onAttachedToEngine 里用引擎的 BinaryMessenger 创建与 Dart 侧同名的 maps_launcher 通道并注册自己;onAttachedToAbility 里持有 UIAbility 引用——startAbilityByType 必须通过 UIAbilityContext 调用,拿不到 context 面板就无从拉起。
参数组装上,两个方法对应系统导航面板的两种场景:launchQuery 映射 sceneType: 3(位置搜索),搜索词放进 destinationName;launchCoordinates 映射 sceneType: 4(地点详情),坐标放进 destinationLatitude / destinationLongitude,上游的可选参数 label 非空时映射为 destinationName。参数缺失时直接 result.error,由 Dart 侧的 _invokeOhos 兜成 false。
面板拉起上,核心调用是 context.startAbilityByType('navigation', wantParam, abilityStartCallback, callback),系统据此弹出导航类应用扩展面板,列出设备上已安装、且声明了对应 linkFeature 的地图应用。'navigation' 是系统能力类型标识,wantParam 的字段名是系统约定,参考华为官方文档"通过 startAbilityByType 拉起导航类应用"。
回调处理上藏着适配里最容易踩的坑:startAbilityByType 有两条回调路径,AbilityStartCallback 的 onError / onResult,以及末尾的同步 AsyncCallback,两者可能先后触发;而 MethodResult 只允许回复一次,回复两次会崩溃。代码用 replyOnce 标志位保证只有第一次结果生效,无论哪条路径先回来。
最后检查 index.ets,确保插件类被导出:
import MapsLauncherPlugin from './src/main/ets/components/plugin/MapsLauncherPlugin';
export default MapsLauncherPlugin;
宿主工程构建时按 pubspec.yaml 的 pluginClass: MapsLauncherPlugin 找到这个导出并自动注册。漏掉这一行的话,Dart 侧调用会收到 MissingPluginException,这也是排查"通道不通"的第一站。
2.5 补全适配说明文件并提交分支
代码之外,社区要求适配仓库补齐四份说明文件,方便使用者和入库审核了解适配情况:
| 文件 | 作用 |
|---|---|
README.OpenSource | 第三方开源组件声明:名称、协议、版本、上游地址 |
README.OpenHarmony_CN.md | 中文适配说明:简介、下载安装、约束与限制、接口说明、遗留问题、目录结构 |
README.OpenHarmony.md | 英文版适配说明,内容与中文版对应 |
CHANGELOG.OpenHarmony.md | 鸿蒙适配版本变更记录,每个 TAG 一节 |
README.OpenSource 是 JSON 格式,本文的实际内容:
[
{
"Name": "maps_launcher",
"License": "MIT License",
"License File": "LICENSE",
"Version Number": "3.0.0+1",
"Owner": "qiaomu8559968@126.com",
"Upstream URL": "https://github.com/pikaju/flutter-maps-launcher",
"Description": "Simple Flutter plugin to open the maps application (or browser) on all platforms."
}
]
CHANGELOG.OpenHarmony.md 每个 TAG 一节,本次内容:
## 3.0.0-ohos-1.0.0-beta.1
* Adapted maps_launcher 3.0.0+1 for the OpenHarmony platform, opening the system navigation-app chooser panel via startAbilityByType for launchQuery and launchCoordinates.
README.OpenHarmony_CN.md 按社区模板组织章节:简介、下载安装(给出 git 依赖写法与 TAG 对照表)、约束与限制(实测通过的框架与 SDK 版本、权限要求)、使用示例、接口说明(表格列出每个接口的参数与返回值)、新增特性(鸿蒙侧的行为描述)、遗留问题(坐标系约定、无可用应用时的行为、交互差异)、目录结构、贡献代码。
文件就绪后提交分支并打 TAG:
git add .
git commit -m "feat: adapt maps_launcher for the OpenHarmony platform"
git push -u atomgit feat/ohos_maps_launcher_3.0.0
# TAG 命名规则:原库版本-ohos-版本号-beta.x
git tag 3.0.0-ohos-1.0.0-beta.1
git push atomgit 3.0.0-ohos-1.0.0-beta.1
TAG 命名与分支命名保持同一套规则:TAG 里能看到原库版本与适配序号,分支名里能看到平台、库名与版本,使用方在 pubspec 里锁定 TAG 即可精确引用某个适配版本。


图四:AtomGit 仓库的分支与 TAG 页面
三、在 Demo 中验证适配效果
3.1 使用仓库自带的 example
上游仓库根目录自带 example 工程,适配时直接用它验证,不需要另建 Demo。example 的 pubspec.yaml 通过相对路径引用插件本身:
dependencies:
maps_launcher:
path: ../
这种本地引用让 example 始终跑在当前目录的插件代码上,改完实现立刻可验。适配分支里 example 已经补好了 ohos 宿主目录(example/ohos),演示页也做成了鸿蒙功能的专属演示:一个地点搜索输入框加"launchQuery:搜索地点"按钮,一张"上海陆家嘴"坐标卡片带"launchCoordinates"按钮,每次调用的结果用 SnackBar 提示成功或失败。
构建并安装到真机:
cd example
flutter pub get
flutter build hap --release
hdc install build/app/outputs/default/entry-default-signed.hap
hdc shell aa start -b com.example.example -a EntryAbility
应用启动后,Flutter 演示页正常渲染:

图五:example 在 HUAWEI nova 12 Ultra 真机正常启动(HarmonyOS 6.1.0 / API 24)
3.2 自建工程时以 AtomGit 链接方式引入
不用仓库自带 example、想在已有工程里验证时,在 pubspec.yaml 中以 AtomGit 仓库链接方式添加 git 依赖:
dependencies:
maps_launcher:
git:
url: https://atomgit.com/oh-flutter/maps_launcher.git
# ref: 根据下方表格选择不同框架适配的 TAG 版本
ref: 3.0.0-ohos-1.0.0-beta.1
flutter pub get
TAG 命名规则是"原库版本-ohos-版本号-beta.x",不同框架版本的 TAG 对照表:
| Flutter 框架版本 | TAG 名称 | 分支名 |
|---|---|---|
| 3.44 | 3.0.0-ohos-1.0.0-beta.1 | feat/ohos_maps_launcher_3.0.0 |
说明:该 TAG 已在 3.41 系 stable(
3.41.10-ohos-1.0.1)真机上实测通过,3.41 用户可直接使用;3.44 canary 引擎的 ArkTS 层要求 API 26,需将宿主工程compatibleSdkVersion调整为26.0.0(见 4.2 的 Q3)。
3.3 调用接口并观察真机运行效果
两个公开接口的调用方式与任何平台一致,先引入再调用:
import 'package:maps_launcher/maps_launcher.dart';
// 按名称搜索地点
final ok = await MapsLauncher.launchQuery('上海东方明珠');
// 展示指定坐标,label 可选
final ok2 = await MapsLauncher.launchCoordinates(31.239717, 121.499876, '上海陆家嘴');
在 example 的真机演示里点击"launchQuery:搜索地点"按钮,系统弹出导航应用选择面板,列出设备上已安装的地图应用:

图六:launchQuery 拉起的系统导航应用选择面板
选择花瓣地图,直达"上海东方明珠"的搜索结果页:

图七:面板中选择花瓣地图后进入搜索结果页
点击"launchCoordinates"按钮,实测设备上系统提示"暂无可用导航方式":

图八:launchCoordinates 在实测设备上提示暂无可用导航方式
这个现象不是适配缺陷。面板里可选哪些应用,由各地图应用在自身 module.json5 中声明的 linkFeature 决定,坐标详情场景要求应用声明 DetailLocation 能力,而实测设备上的地图应用均只声明了 PlaceSearch(位置搜索),面板自然无可选项。此时拉起动作本身已由系统完成,接口仍返回 true;换用 launchQuery 即可正常唤起。随着地图应用逐步补齐 linkFeature 声明,坐标场景会自动可用,机制细节见姊妹篇的"工作原理"章节。
到这里,适配完成度就有了真机背书:两个接口都能调通,搜索场景全链路可用(面板弹出、应用选择、结果直达),坐标场景受制于生态现状但拉起语义正确,true / false 返回值与各平台一致。
四、常见问题
4.1 适配过程中的问题
Q1:flutter create --platforms ohos . 会不会改动我现有的代码?
只会新增 ohos/ 目录,并在 pubspec.yaml 的 plugin.platforms 下追加一个 ohos 节点,android/、ios/、lib/ 等原有内容不会被改写。如果对生成的模板不满意,删掉 ohos/ 目录重新执行即可,pubspec 里多出的节点手动删掉也无碍。
Q2:Dart 侧调用报 MissingPluginException,通道没通?
按顺序排查三处:pubspec.yaml 是否有 ohos: 节点且 pluginClass 拼写正确;ohos/index.ets 是否导出了插件类;插件类名与 pluginClass 是否一致。三者任一不符,宿主的自动注册都会静默失败。此外工程需要重新执行 flutter pub get 并全量构建,热重载不会触发插件重新注册。
Q3:compatibleSdkVersion 相关的构建或安装报错?
两套环境组合对应不同要求:3.41 系 stable 搭配 compatibleSdkVersion: 6.1.0(23)(注意保留带括号的旧格式);3.44 canary 引擎的 ArkTS 层要求 API 26,需改为 26.0.0。真机安装报"此应用暂不支持在当前设备安装"时,检查 compatibleSdkVersion 是否高于真机 API 版本(本文真机为 API 24)。
Q4:ArkTS 侧 result.success 被调用两次导致崩溃?
startAbilityByType 的 AbilityStartCallback(onError / onResult)与末尾的同步 AsyncCallback 可能先后触发,而 MethodResult 只允许回复一次。参照 2.4 的实现,用 replyOnce 标志位包裹所有回复路径,第一次结果生效后其余丢弃。
4.2 使用过程中的问题
Q1:launchCoordinates 提示"暂无可用导航方式"是怎么回事?
面板内可选哪些应用,由各地图应用声明的 linkFeature 决定(Navigation、RoutePlan、PlaceSearch、DetailLocation 四类)。实测设备上的地图应用只声明了 PlaceSearch,坐标详情场景(sceneType 4)没有可选应用,但拉起动作已完成,接口返回 true。换用 launchQuery 即可正常唤起,详见 3.3 的图六至图八。
Q2:返回 true 就代表地图应用被打开了吗?
true 表示"拉起动作成功完成"(系统回调确认),不保证面板里有应用可选,也不代表某个具体地图应用完成了展示。对可用性敏感的业务,建议在目标机型上结合实际场景做功能实测。
Q3:中国大陆的坐标为什么偏了几百米?
坐标系问题。鸿蒙导航面板在中国大陆要求 GCJ-02,而多数业务数据(GPS 采集或国际地图服务)是 WGS-84,两者相差数百米。调用前先做 WGS-84 → GCJ-02 转换,完整转换实现见姊妹篇的使用章节;中国大陆以外的坐标原样透传即可。
五、结语
回顾整条适配链路:上游仓库同步进 AtomGit 保住历史,flutter create --platforms ohos . 一条命令补全目录结构,改动只落在 lib/maps_launcher.dart 的入口分支与 ohos/src/main/ets/components/plugin/MapsLauncherPlugin.ets 这一个新文件里,四份说明文件交代清楚适配行为,分支与 TAG 按社区规范提交发布。原库的接口签名、返回值语义与各平台行为原封未动,这正是"只做加法"的适配给使用方的承诺。
适配过程中发现的问题欢迎到 maps_launcher 鸿蒙仓库提 Issue(鸿蒙适配层)或 原库 GitHub 仓库提 Issue(原库行为),修复代码欢迎发 PR。接口的完整用法、坐标系转换代码与业务侧最佳实践,见姊妹篇《maps_launcher 的鸿蒙使用指南》。
六、相关链接
欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:
更多推荐




所有评论(0)