pedometer(4.2.0)做两件事:把自开机以来的累计步数推成流,以及判断"人在走"还是"已停下"。它的 API 面很小——两个静态 getter,连一个 MethodChannel 都没有——但它在鸿蒙上暴露了一个很典型的问题:同一份 Dart 代码,在其他平台上多跑了一段逻辑,鸿蒙上那段不会跑。不去把它补回来,界面就会一直停在"行走中"。

环境准备:本文只讲适配本身,不重复环境搭建步骤。Flutter for OpenHarmony SDK、DevEco Studio、模拟器/真机的完整配置见官方指引:
https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/docs/ohos/getting-started/flutter-oh-env-setup.md

在这里插入图片描述


一、适配步骤

1️⃣ 上游仓库同步到 AtomGit

上游 cph-cachet/flutter-plugins 是一个含 17 个包的大 monorepo,pedometer 只是其中之一。做法是:在 AtomGit 的 oh-flutter 组织下新建空仓库 pedometer,完整克隆上游(保留全部历史)后再追加适配提交。

git clone https://github.com/cph-cachet/flutter-plugins.git pd_work
cd pd_work
git rev-list --count HEAD      # 1730,上游历史一条不少

实测提醒:这个仓库体量偏大,用 https://ghproxy.net/ 克隆连续两次被掐断(fetch-pack: unexpected disconnect / fatal: early EOF / invalid index-pack output)。换 https://gh-proxy.com/ 一次成功。遇到同类报错不要反复重试同一个代理。

2️⃣ 本地克隆目标库

cd packages/pedometer
flutter pub get

包本身没有任何第三方依赖,只有 flutter SDK 和 flutter_test,解析很快。

3️⃣ 创建分支并用框架命令补全鸿蒙目录

git checkout -b feat/ohos_pedometer_4.2.0
cd packages/pedometer
# 插件本体 → HAR 骨架
flutter create -t plugin --platforms ohos --org com.example --project-name pedometer .
# 示例工程 → app 骨架
cd example
flutter create --platforms ohos --project-name pedometer_example .

-t plugin 不能省:不加会按 app 模板生成(出来的是 AppScope/ + entry/,而不是插件要的 index.ets + src/main/ets/components/plugin/PedometerPlugin.ets),并且会把 .metadata 写成 project_type: app,之后再补 -t plugin 会直接报 The requested template type 'plugin' doesn't match the existing template type of 'app'。

生成的骨架上,本次真正要动的只有一个文件:

packages/pedometer/
├── ohos/                                      # ★ 新增(HAR)
│   ├── index.ets                              # 只做导出,一行
│   ├── oh-package.json5
│   ├── build-profile.json5
│   └── src/main/
│       ├── module.json5
│       └── ets/components/plugin/
│           └── PedometerPlugin.ets            # ★ 全部 ArkTS 代码
├── example/ohos/                              # ★ 新增(示例的鸿蒙工程)
└── pubspec.yaml                               # 加 2 行 ohos 声明

4️⃣ 适配详细过程

flutter create 会顺手灌进一批模板文件,必须逐个清掉。这次多出 23 项,例如:

lib/pedometer_method_channel.dart        # 与 lib/ 下真实实现重名(本库根本不用 MethodChannel)
lib/pedometer_platform_interface.dart
test/pedometer_method_channel_test.dart
ios/pedometer/Sources/pedometer/PrivacyInfo.xcprivacy
example/integration_test/
example/ios/RunnerTests/  example/android/app/src/main/res/drawable-v21/ ...

清理办法:取 git status --porcelain 里的 ?? 项,排除含 ohos/ 的路径与 .metadata(它是 M 状态,记录 ohos 平台,要保留),其余全删。原库的 lib/、test/、各平台实现一个字节都没动。

除了新写的 PedometerPlugin.ets,改动只有三处:插件的 pubspec.yaml 加 ohos: pluginClass: PedometerPlugin;示例的 module.json5 加运动权限声明;示例的 main.dart 里给 permission_handler 调用加一道平台判断(原因见下)。

5️⃣ 补全适配仓库所需文件

新增 README.OpenHarmony_CN.md 与 README.OpenHarmony.md:安装方式(monorepo 必须带 path)、权限声明要求、三个关键点、已知限制、验证结果与复现命令。上游原有文档未做改动。

6️⃣ 代码推送

git add packages/pedometer/ohos packages/pedometer/example/ohos \
        packages/pedometer/{pubspec.yaml,.metadata,README.OpenHarmony*.md} \
        packages/pedometer/example/{.metadata,lib/main.dart}
git commit -m "feat: 新增OpenHarmony平台实现"
git push origin feat/ohos_pedometer_4.2.0
git push origin HEAD:main        # 上游克隆下来默认分支是 master,要显式推成 main
git tag -a 4.2.0-ohos-1.0.0-beta.1 -m "pedometer 4.2.0 OpenHarmony 适配"
git push origin 4.2.0-ohos-1.0.0-beta.1

推送前必做:清空 example/ohos/build-profile.json5 里的 signingConfigs(里面是明文密码)。

二、上游给了什么契约

packages/pedometer/lib/pedometer.dart 里只有两个 getter,两条通道都是 EventChannel:

class Pedometer {
  static const EventChannel _stepDetectionChannel = EventChannel('step_detection');
  static const EventChannel _stepCountChannel     = EventChannel('step_count');

  static Stream<PedestrianStatus> get pedestrianStatusStream =>
      _stepDetectionChannel.receiveBroadcastStream().map((event) => PedestrianStatus._(event));

  static Stream<StepCount> get stepCountStream =>
      _stepCountChannel.receiveBroadcastStream().map((event) => StepCount._(event));
}
Dart 侧通道事件值Android 原生
stepCountStreamstep_countint 累计步数Sensor.TYPE_STEP_COUNTER
pedestrianStatusStreamstep_detectionint:0=stopped,1=walkingSensor.TYPE_STEP_DETECTOR

两个 DTO 的解析都写得很紧,这是必须留意的:

StepCount._(dynamic e) {
  _steps = e as int;          // 非整数直接抛类型错误
}

PedestrianStatus._(dynamic t) {
  int _type = t as int;
  _status = _STATUSES[_type]!;   // _STATUSES 只有 {0: 'stopped', 1: 'walking'}
}

as int 意味着原生侧不能把步数当成 double 发;_STATUSES[_type]! 意味着状态只能是 0 或 1——传个 2 会因为 ! 断言崩溃。

还有一处藏在 pedestrianStatusStream 里的分支,它是本次适配的核心:

static Stream<PedestrianStatus> get pedestrianStatusStream {
  Stream<PedestrianStatus> stream = _stepDetectionChannel
      .receiveBroadcastStream()
      .map((event) => PedestrianStatus._(event));
  if (Platform.isAndroid) return _androidStream(stream);   // ← 只有 Android 走这段
  return stream;                                           // ← 鸿蒙走这里,原样透传
}

_androidStream 是一个去抖器:每收到一次步伐就发 walking,并起一个 2 秒定时器;2 秒内没有新步伐就把状态切成 stopped。也就是说 Android 原生只负责"每步发一个事件",walking/stopped 的判定是 Dart 侧做的。

鸿蒙不在这条分支里 → 这段去抖逻辑不会执行 → 原生侧必须自己实现。

三、鸿蒙侧的 API 选型:三个关键点

关键点一:walking/stopped 的去抖要在原生补回来

@ohos.sensor 提供了两个对应的传感器:

SensorId.PEDOMETER_DETECTION = 265   // 步伐检测,回调带 scalar
SensorId.PEDOMETER           = 266   // 计步,回调带 steps

两个都需要 ohos.permission.ACTIVITY_MOTION。订阅方式和别的传感器一样:

sensor.on(sensor.SensorId.PEDOMETER_DETECTION, this.onStepDetected);
sensor.on(sensor.SensorId.PEDOMETER, this.onStepCount);

因为 Dart 侧在鸿蒙上不做去抖,原生这边把"收到步伐 → walking,静默 2 秒 → stopped"整套实现出来,参数与上游 Android 完全对齐:

private handleStepDetected(): void {
  this.emitStatus(STATUS_WALKING);
  this.clearStopTimer();
  this.stopTimer = setTimeout((): void => {
    this.stopTimer = -1;
    this.emitStatus(STATUS_STOPPED);
  }, STOP_TIMEOUT_MS);          // 2000,与上游 Timer(Duration(seconds: 2)) 一致
}

这一步不做,界面上"Pedestrian Status"会永远停在 walking(只要用户走过一次),因为没有任何东西会把状态改回 stopped。这类"平台分支导致逻辑缺一块"的问题,光看原生接口是发现不了的,必须把 Dart 层读完。

顺带说一下为什么不做状态去重:Android 侧每条步伐都会推一个事件,调用方按"收到即走了一步"来处理是合法用法;鸿蒙侧保持同样语义,只在状态真正切换时打日志。

关键点二:部分设备没有独立的步伐检测传感器

SensorId.PEDOMETER_DETECTION 在 Pura X View 模拟器上订阅会抛错:

PedometerPlugin --> PEDOMETER_DETECTION unavailable code=401, fallback to PEDOMETER changes

401 The parameter invalid —— 模拟器只模拟了计步传感器,没有模拟步伐检测。直接把这个错误抛给 Dart,step_detection 通道就彻底不可用了。

所以加了一条降级路径:检测传感器不可用时,用计步值的变化来推导行走状态——steps 变了就说明又走了一步,接同样的 2 秒去抖:

private handleStepCount(data: sensor.PedometerResponse): void {
  const steps: number = Math.floor(data.steps);

  // 降级路径:没有 PEDOMETER_DETECTION 时,计步值增加即视为"又走了一步"
  if (this.detectionFromCount && this.detectionSink !== null && steps !== this.lastSteps) {
    if (this.lastSteps !== -1) {
      this.handleStepDetected();
    }
    this.lastSteps = steps;
  }
  // ...再推给 step_count 通道
}

这里有个刻意的取舍:降级路径不自己再去订阅一次 PEDOMETER。因为 step_count 通道本来就会订阅同一个传感器,重复订阅容易触发重复回调,而且两个通道取消时会互相注销。代价是降级路径要求计步通道也在监听(示例同时订阅了两条通道),这一条写进了已知限制。

关键点三:ACTIVITY_MOTION 是 user_grant 权限,声明必须带 reason

这是构建期硬报错,不是运行时问题。只在 module.json5 里写个权限名:

{"name": "ohos.permission.ACTIVITY_MOTION"}

构建直接失败:

Error Message: The reason and usedScene attributes are mandatory for user_grant permissions.

正确写法:

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

reason 指向字符串资源,会原样显示在系统授权弹窗上——所以这句文案是给用户看的,要写人话:

{ "name": "activity_motion_reason", "value": "用于统计步数与识别行走状态" }

弹窗实测长这样:

允许"pedometer_example"访问你的运动数据?
用于统计步数与识别行走状态
                          [不允许]  [允许]

这一点顺带说明:reason 不是形式要求,它是弹窗正文。写"权限申请"之类的空话,用户看到的就是空话。

权限的运行时申请由插件自己做:插件实现 AbilityAware 拿到 UIAbilityContext,在订阅时调 abilityAccessCtrl.requestPermissionsFromUser()。

onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.context = binding.getAbility().context as common.UIAbilityContext;
}
const atManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(context, [PERMISSION_ACTIVITY_MOTION])
  .then((result) => {
    const results: Array<number> = result.authResults;
    // authResults[i] === 0 表示已授权
  })

所以宿主应用不需要 permission_handler(见下一节)。

四、示例工程的两处必要改动

把 permission_handler 从鸿蒙路径上摘掉

上游示例开头就申请运动权限:

Future<bool> _checkActivityRecognitionPermission() async {
  bool granted = await Permission.activityRecognition.isGranted;
  if (!granted) {
    granted = await Permission.activityRecognition.request() == PermissionStatus.granted;
  }
  return granted;
}

permission_handler 目前没有鸿蒙实现,这个调用在鸿蒙上不返回,initPlatformState() 卡在它后面,两条流永远订阅不上——界面表现就是两个 ?,而且日志里看不出任何异常。

由于权限已经由插件自身申请,这里只需要在鸿蒙上跳过:

Future<bool> _checkActivityRecognitionPermission() async {
  // 鸿蒙侧不经过 permission_handler:ACTIVITY_MOTION 由插件自身在订阅时申请
  if (!Platform.isAndroid) {
    return true;
  }
  bool granted = await Permission.activityRecognition.isGranted;
  ...
}

这属于"示例为了在鸿蒙上可运行"的必要改动,不是库的能力缺失。

如何引用适配库

本仓库是 monorepo,包在 packages/pedometer,下游用 git 引用时必须带 path:

dependencies:
  pedometer:
    git:
      url: https://atomgit.com/oh-flutter/pedometer.git
      ref: 4.2.0-ohos-1.0.0-beta.1
      path: packages/pedometer

示例工程内部本来就用的是 pedometer: path: ../,无需改动。

编译构建

cd packages/pedometer/example
flutter pub get
flutter build hap --debug --target-platform ohos-x64

首次构建前先生成调试签名(仓库里 signingConfigs 已清空):

cd ohos && devecocli signature generate && cd ..
hdc shell power-shell wakeup
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.pedometer_example

五、真机验证

验证在 HarmonyOS 7.0.0(26.0.0) Beta2 的 API 26 模拟器(ohos-x64)上完成。这个库的好处是模拟器支持注入步数,不用真机也能把数据链路走通。

权限:首次启动弹出系统授权框,正文就是上面那句 reason。

点「允许」后,原生日志确认订阅与降级路径:

FlutterEngineCxnRegistry --> Adding plugin: PedometerPlugin
PedometerPlugin --> pedometer channels registered
PedometerPlugin --> PEDOMETER_DETECTION unavailable code=401, fallback to PEDOMETER changes
PedometerPlugin --> step count subscribed

在这里插入图片描述

步数通道:注入 500 步。

devecocli emulator sensor --target "Pura X View" --steps 500

界面 Steps Taken 由 ? 变为 500,同时状态切到 walking。

在这里插入图片描述

2 秒去抖:不再注入,2 秒后状态自动变 stopped。

PedometerPlugin --> pedestrian status -> walking    17:06:20
PedometerPlugin --> pedestrian status -> stopped    17:06:22

两次日志相隔正好 2 秒,说明原生侧复刻的去抖生效了。再注入一次 900 步,重复同样的过程:

PedometerPlugin --> pedestrian status -> walking    17:06:31
PedometerPlugin --> pedestrian status -> stopped    17:06:33

界面此时显示 Steps Taken = 900、Pedestrian Status = stopped。

在这里插入图片描述

在这里插入图片描述

六、已知限制

  • 降级路径要求计步通道也在监听。没有 PEDOMETER_DETECTION 的设备上,行走状态来自 PEDOMETER 的变化;若调用方只订阅 step_detection 而不订阅 step_count,就不会有状态事件。两条都订阅(常规用法)则无影响。
  • 有 PEDOMETER_DETECTION 时优先用它,此时不依赖计步通道。
  • 示例移除了鸿蒙侧的 permission_handler 调用,权限改由插件自身申请;permission_handler 的依赖仍留在 pubspec.yaml 中(Android 侧仍在用)。
  • 步数是"自开机以来"的累计值,语义由系统传感器决定,插件不做跨重启的持久化——这一点与上游 Android 一致,不是适配引入的差异。

七、常见问题

Q:为什么鸿蒙上 Pedestrian Status 一直是 walking,停不下来?
A:因为上游的去抖写在了 if (Platform.isAndroid) 分支里,鸿蒙不在这个分支,Dart 侧不会把状态切回 stopped。适配里在原生用 setTimeout(2000) 补上了这套逻辑,日志中能看到 walking → stopped 相隔 2 秒。

Q:step_detection 订阅报 code=401 The parameter invalid 是什么原因?
A:设备没有独立的步伐检测传感器。Pura X View 模拟器只模拟了计步,所以 SensorId.PEDOMETER_DETECTION 订阅失败。适配里有降级路径:用计步值变化推导状态,功能不丢失。

Q:为什么步数必须取整?
A:Dart 侧是 _steps = e as int,如果原生发的是 double 会直接抛类型错误。同理状态只发 0/1——PedestrianStatus._ 用的是 _STATUSES[_type]!,其他值会触发断言崩溃。

Q:声明 ACTIVITY_MOTION 为什么构建报 reason and usedScene are mandatory?
A:它是 user_grant 权限,鸿蒙要求声明时必须给出申请理由与使用场景。reason 指向字符串资源,会显示在系统授权弹窗上;usedScene 说明哪个 ability、什么时候用。

Q:需要 permission_handler 吗?
A:不需要。插件实现了 AbilityAware,在订阅时用 abilityAccessCtrl.requestPermissionsFromUser() 自行申请;宿主应用只需在 module.json5 里声明权限和理由。上游示例里的 permission_handler 调用在鸿蒙上会卡住流程,已在示例中按平台跳过。

Q:模拟器上怎么造数据?
A:devecocli emulator sensor --target "Pura X View" --steps 500。注意每次注入都是设置绝对值,连续注入不同值才能触发"步数变化"。

Q:模拟器验过还要真机吗?
A:建议补真机,主要验两件事:一是真实设备的 PEDOMETER_DETECTION 是否可用(走的是另一条代码路径),二是计步的累积行为与系统省电策略的关系——模拟器的注入不会反映这些。

小结

这个库的适配代码不到 250 行,但把一类容易被忽略的问题讲清楚了:平台分支会让 Dart 层少跑一段逻辑。Platform.isAndroid 那半行代码,在鸿蒙上等于"去抖器不存在",只看原生接口是发现不了的——必须把 Dart 层读到 Platform 判断的位置。

另外两条也都来自"平台差异"而非"接口差异":模拟器没有步伐检测传感器(要靠降级路径兜住),以及 user_grant 权限的 reason 会直接展示给用户(所以它是文案,不是形式)。


本篇用到的库

项内容
三方库pedometer(4.2.0 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-flutter/pedometer
适配 TAG4.2.0-ohos-1.0.0-beta.1
适配分支feat/ohos_pedometer_4.2.0
包路径packages/pedometer(monorepo,引用时需带 path)
dependencies:
  pedometer:
    git:
      url: https://atomgit.com/oh-flutter/pedometer.git
      ref: 4.2.0-ohos-1.0.0-beta.1
      path: packages/pedometer

验证环境

项版本
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.0.0.621(API 26)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64)
注入工具devecocli emulator sensor

欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

Flutter 三方库鸿蒙适配清单:https://atomgit.com/oh-flutter/flutter-ohos-adaptation-checklist

Logo

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

更多推荐