Flutter/OpenHarmony 三方库适配:geolocator 定位能力

定位能力是 Flutter 应用迁移 OpenHarmony 时最容易遇到平台差异的模块之一。Dart 层的 Geolocator API 看起来是统一的,但权限模型、定位服务开关、位置回调和设置页都由系统提供。本文记录 geolocator 的 OHOS 适配过程,重点解释为什么必须同时处理 Dart 注册、MethodChannel、EventChannel、UIAbilityContext 和系统权限,而不是只让一个方法返回成功。

一、项目信息与适配目标

项目内容
仓库名称geolocator
上游版本14.1.1
OHOS 适配 TAG14.1.1-ohos-1.0.0-beta.1
适配分支main
原生模块OHOS HAR,入口类 GeolocatorOhosPlugin
Dart 注册类GeolocatorOhos

首个版本的目标是完整覆盖前台定位闭环:

检查定位服务
      |
检查/请求定位权限
      |
获取当前位置或最后位置
      |
订阅位置流和服务开关流
      |
在设置页中调整定位配置

后台常驻定位、Android 前台服务和精确位置的特殊审批不在首版范围内。适配遇到系统没有等价能力时会返回明确的错误码,不会把未完成的能力伪装成成功。

二、先保住上游 Dart API

上游 geolocator 已经有稳定的 Geolocator 静态 API,业务代码通常这样调用:

import 'package:geolocator/geolocator.dart';

Future<Position> loadPosition() async {
  final enabled = await Geolocator.isLocationServiceEnabled();
  if (!enabled) {
    throw StateError('定位服务已关闭');
  }

  var permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }

  if (permission == LocationPermission.whileInUse ||
      permission == LocationPermission.always) {
    return Geolocator.getCurrentPosition();
  }
  throw StateError('没有定位权限');
}

OHOS 适配不重新设计 Dart API,只在主包中注册一个薄的实现类:

import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';
// ignore: implementation_imports
import 'package:geolocator_platform_interface/src/implementations/method_channel_geolocator.dart';

class GeolocatorOhos extends MethodChannelGeolocator {
  static void registerWith() {
    GeolocatorPlatform.instance = GeolocatorOhos();
  }
}

这样 Android、iOS 业务页面继续使用原来的 Geolocator 调用方式,OHOS 只替换平台实现。pubspec.yaml 必须同时声明原生入口和 Dart 注册类,否则 Flutter 工具链可能只生成了 OHOS 工程,却没有在 Dart 侧选择 OHOS 实现:

flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: GeolocatorOhosPlugin
        dartPluginClass: GeolocatorOhos

三、三个通道的职责

OHOS 插件注册了三个通道:

通道用途
flutter.baseflow.com/geolocator权限、服务开关、单次定位、设置页
flutter.baseflow.com/geolocator_updates连续位置更新
flutter.baseflow.com/geolocator_service_updates定位服务开关变化

MethodChannel 适合一次请求和一次响应,位置流必须使用 EventChannel。原生入口在引擎绑定时创建通道,在引擎分离时注销 handler、停止定位回调并清空 sink,避免页面销毁后仍然持有系统回调。

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.methodChannel = new MethodChannel(
    binding.getBinaryMessenger(),
    METHOD_CHANNEL,
  );
  this.updateChannel = new EventChannel(
    binding.getBinaryMessenger(),
    UPDATE_CHANNEL,
  );
  this.serviceUpdateChannel = new EventChannel(
    binding.getBinaryMessenger(),
    SERVICE_UPDATE_CHANNEL,
  );
  this.methodChannel.setMethodCallHandler(this);
  this.updateChannel.setStreamHandler(this);
  this.serviceUpdateChannel.setStreamHandler(this.serviceHandler);
}

四、权限不能只看 Dart 的枚举

示例工程在 entry/src/main/module.json5 中声明近似定位权限:

"requestPermissions": [
  {
    "name": "ohos.permission.APPROXIMATELY_LOCATION",
    "reason": "$string:location_permission_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

原生层使用 bundleManager.getBundleInfoForSelfSync 获取当前应用的 accessToken,再用 abilityAccessCtrl 检查权限:

private checkPermission(permission: string): boolean {
  const info = bundleManager.getBundleInfoForSelfSync(0);
  return abilityAccessCtrl.createAtManager().checkAccessTokenSync(
    info.appInfo.accessTokenId,
    permission as ESObject,
  ) === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}

请求权限时必须持有当前 Ability 的 Context,不能只使用 ApplicationContext:

private requestPermission(result: MethodResult): void {
  const context = this.uiAbilityContext;
  if (context === null) {
    result.error(
      'ACTIVITY_MISSING',
      'UIAbilityContext is not available.',
      null,
    );
    return;
  }

  abilityAccessCtrl.createAtManager()
    .requestPermissionsFromUser(
      context,
      [APPROXIMATELY_LOCATION] as ESObject,
    )
    .then((requestResult: ESObject): void => {
      const granted =
        requestResult.authResults.length > 0 &&
        requestResult.authResults[0] === 0;
      result.success(granted ? 3 : 0);
    });
}

这里把 OHOS 的授权结果映射回上游枚举:3 表示 whileInUse,0 表示 denied。用户永久拒绝时,适配层会记录 permissionDeniedForever,下一次 checkPermission() 返回上游的 deniedForever 语义。

五、当前位置和最后位置

当前位置使用 geoLocationManager.getCurrentLocation(),最后位置使用 getLastLocation():

private getCurrentPosition(result: MethodResult): void {
  geoLocationManager.getCurrentLocation()
    .then((location: geoLocationManager.Location): void => {
      result.success(this.positionMap(location));
    })
    .catch((error: ESObject): void => {
      result.error(
        this.locationErrorCode(error),
        this.errorMessage(error),
        null,
      );
    });
}

private getLastKnownPosition(result: MethodResult): void {
  try {
    result.success(
      this.positionMap(geoLocationManager.getLastLocation()),
    );
  } catch (error) {
    result.success(null);
  }
}

positionMap 把 OHOS 的 Location 字段转换成上游 Position 所需的 map。OHOS 当前没有公开等价的 headingAccuracy 和 speedAccuracy,适配返回 -1 表示未知:

latitude       <- Location.latitude
longitude      <- Location.longitude
timestamp      <- Location.timeStamp
accuracy       <- Location.accuracy
altitude       <- Location.altitude
heading        <- Location.direction
speed          <- Location.speed
headingAccuracy<- -1
speedAccuracy  <- -1
isMocked       <- false

最后位置不存在时返回 null,这和上游 API 对“没有缓存位置”的表达一致。

六、位置流和服务开关流

位置流监听 locationChange,服务开关流监听 locationEnabledChange。每次 onListen 都要检查权限,并防止重复订阅:

onListen(args: Any, events: EventSink): void {
  if (!this.hasApproximatePermission()) {
    events.error(
      'PERMISSION_DENIED',
      'Location permission is not granted.',
      null,
    );
    return;
  }
  if (this.locationCallback !== null) {
    events.error(
      'LOCATION_SUBSCRIPTION_ACTIVE',
      'A location stream is already active.',
      null,
    );
    return;
  }

  this.locationSink = events;
  this.locationCallback = (
    location: geoLocationManager.Location,
  ): void => {
    this.locationSink?.success(this.positionMap(location));
  };
  geoLocationManager.on(
    'locationChange',
    {},
    this.locationCallback,
  );
}

onCancel 和引擎分离时都要调用 offLocationChange,并将 callback、sink 置空。服务状态流则把 OHOS 的布尔值转换为上游的 1/0,Dart 层再映射为 ServiceStatus.enabled 和 ServiceStatus.disabled。

七、设置页与生命周期

openAppSettings() 和 openLocationSettings() 通过系统设置 Ability 打开对应页面:

context.startAbility({
  bundleName: 'com.huawei.hmos.settings',
  abilityName: 'com.huawei.hmos.settings.MainAbility',
  uri: 'application_location_settings',
}).then((): void => result.success(true))
  .catch((): void => result.success(false));

系统发行版可能调整设置页 URI,所以打开失败不会抛出未处理异常,而是返回 false。实现类实现 AbilityAware:

onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.uiAbilityContext = binding.getAbility().context;
}

onDetachedFromAbility(): void {
  this.uiAbilityContext = null;
}

这一步是定位权限弹窗和设置页跳转真正可用的前提。

八、测试结果

新增 Dart 测试覆盖 OHOS 注册、权限映射、服务开关和位置字段:

flutter test \
  test/geolocator_ohos_test.dart \
  test/ohos_plugin_metadata_test.dart

结果:All tests passed。

flutter analyze

结果:无 error。

示例工程生成 OHOS HAP 后,安装并启动:

cd geolocator/example
flutter build hap --debug
hdc install -r ohos/entry/build/default/outputs/default/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b com.example.geolocator_example

在这里插入图片描述

九、能力边界与排查建议

  • 首版只覆盖前台定位,不实现后台常驻定位。
  • always 不会伪造为已授权,前台应用统一返回 whileInUse 语义。
  • 定位服务关闭、权限弹窗未完成和系统定位接口异常会返回不同错误码。
  • 如果方法通道没有返回,先检查 pubspec.yaml 的 ohos.pluginClass、ohos.dartPluginClass、HAR 的 index.ets 和 AbilityContext 是否绑定。
  • 如果权限一直 denied,检查应用工程的 module.json5、权限 reason 字符串和系统设置中的授权状态。

社区

  • OpenHarmony Flutter 社区:https://atomgit.com/oh-flutter
  • CPF-Flutter 社区:https://atomgit.com/CPF-Flutter

欢迎提交设备型号、系统版本、复现步骤和 hilog 日志,一起完善 OHOS 定位适配。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐