Flutter for OpenHarmony 实战:三方库 pedometer 的鸿蒙化适配指南
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 原生 |
|---|---|---|---|
stepCountStream | step_count | int 累计步数 | Sensor.TYPE_STEP_COUNTER |
pedestrianStatusStream | step_detection | int:0=stopped,1=walking | Sensor.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 |
| 适配 TAG | 4.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 SDK | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.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
更多推荐


所有评论(0)