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 开发环境搭建指南

适配工作比单纯使用多一项要求:终端里 flutter 命令必须指向 ohos 版 SDK,因为后文自动补全 ohos 目录靠的是它提供的 flutter create --platforms ohos 能力。环境装好后用 flutter devices 确认能识别鸿蒙真机。本文实测使用的环境:

版本
Flutter(ohos 版)3.41.10-ohos-1.0.1
编译 SDK6.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.yamlplugin.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:ioPlatform.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);
    }
  }
}

实现有四个关键点。

生命周期上,插件同时实现 FlutterPluginMethodCallHandlerAbilityAware 三个接口:onAttachedToEngine 里用引擎的 BinaryMessenger 创建与 Dart 侧同名的 maps_launcher 通道并注册自己;onAttachedToAbility 里持有 UIAbility 引用——startAbilityByType 必须通过 UIAbilityContext 调用,拿不到 context 面板就无从拉起。

参数组装上,两个方法对应系统导航面板的两种场景:launchQuery 映射 sceneType: 3(位置搜索),搜索词放进 destinationNamelaunchCoordinates 映射 sceneType: 4(地点详情),坐标放进 destinationLatitude / destinationLongitude,上游的可选参数 label 非空时映射为 destinationName。参数缺失时直接 result.error,由 Dart 侧的 _invokeOhos 兜成 false

面板拉起上,核心调用是 context.startAbilityByType('navigation', wantParam, abilityStartCallback, callback),系统据此弹出导航类应用扩展面板,列出设备上已安装、且声明了对应 linkFeature 的地图应用。'navigation' 是系统能力类型标识,wantParam 的字段名是系统约定,参考华为官方文档"通过 startAbilityByType 拉起导航类应用"。

回调处理上藏着适配里最容易踩的坑:startAbilityByType 有两条回调路径,AbilityStartCallbackonError / onResult,以及末尾的同步 AsyncCallback,两者可能先后触发;而 MethodResult 只允许回复一次,回复两次会崩溃。代码用 replyOnce 标志位保证只有第一次结果生效,无论哪条路径先回来。

最后检查 index.ets,确保插件类被导出:

import MapsLauncherPlugin from './src/main/ets/components/plugin/MapsLauncherPlugin';

export default MapsLauncherPlugin;

宿主工程构建时按 pubspec.yamlpluginClass: 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.443.0.0-ohos-1.0.0-beta.1feat/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.yamlplugin.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 被调用两次导致崩溃?

startAbilityByTypeAbilityStartCallbackonError / 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 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐