Flutter for OpenHarmony 实战:三方库 battery_plus 的鸿蒙化适配指南
battery_plus(上游 7.1.1)是 Flutter 社区使用最广的电量插件之一,提供四个能力:读电量百分比、读充电状态、查是否处于省电模式、监听充电状态变化。
它正好适合拿来讲一类问题:鸿蒙把"状态查询"和"状态变化"拆到了两个完全不同的地方。查询是静态快照,变化却要靠系统公共事件——不知道这一点,事件通道永远收不到东西。
环境准备:本文只讲适配本身,不重复环境搭建步骤。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
上游是 fluttercommunity/plus_plugins(一个包含 battery_plus、connectivity_plus 等多个插件的 monorepo)。本次适配的做法是:在 AtomGit 的 oh-flutter 组织下新建空仓库 battery_plus,然后把上游完整克隆下来、在保留全部上游提交历史的前提下追加适配提交。
git clone https://github.com/fluttercommunity/plus_plugins.git bp_work
cd bp_work
git rev-list --count HEAD # 2356,上游历史一条不少
2️⃣ 本地克隆目标库
用支持鸿蒙的 Flutter SDK 打开工程,确认依赖能正常解析:
cd packages/battery_plus/battery_plus
flutter pub get
3️⃣ 创建分支,并用框架命令补全鸿蒙目录
git checkout -b feat/ohos_battery_plus_7.1.1
cd packages/battery_plus/battery_plus
# 插件本体生成 HAR 骨架
flutter create -t plugin --platforms ohos --org dev.fluttercommunity.plus --project-name battery_plus .
# 示例工程生成 app 骨架
cd example
flutter create --platforms ohos --project-name battery_plus_example .
这里有两个必须注意的点:
其一,-t plugin 不能省。 不加会按"应用"模板生成(出来的是 AppScope/ + entry/ 那一套),而插件需要的是 HAR 骨架(index.ets + src/main/ets/components/plugin/BatteryPlusPlugin.ets)。更麻烦的是,错误的模板会把 .metadata 写成 project_type: app,之后再加 -t plugin 会直接报:
The requested template type 'plugin' doesn't match the existing template type of 'app'.
解决办法是先把误生成的 .metadata 删掉再重来。
其二,--org 要显式给。 工程里已经存在两种组织名(dev.fluttercommunity.plus 和示例的 io.flutter.plugins.batteryexample),不指定 --org 会报 Ambiguous organization in existing files。
生成的骨架里,本次适配真正要动的只有一个文件:
packages/battery_plus/battery_plus/
├── ohos/ # ★ 新增(HAR)
│ ├── index.ets # 只做导出,一行
│ ├── oh-package.json5 # HAR 元信息
│ ├── build-profile.json5
│ └── src/main/
│ ├── module.json5
│ └── ets/components/plugin/
│ └── BatteryPlusPlugin.ets # ★ 全部 ArkTS 代码
├── example/ohos/ # ★ 新增(示例的鸿蒙工程)
└── pubspec.yaml # 加 2 行 ohos 声明
4️⃣ 适配详细过程
新增了什么、为什么,见下面第三、四节。一句话概括:新增了 BatteryPlusPlugin.ets(约 200 行),把上游 Android 的四个能力映射到 @ohos.batteryInfo + @ohos.power + @ohos.commonEventManager;pubspec.yaml 只加了 ohos: pluginClass: BatteryPlusPlugin。
flutter create 会顺手灌进一批模板文件,必须逐个清掉。 这是本次适配最耗时的杂活,实测多出来的有 26 项,包括:
lib/battery_plus_method_channel.dart # 和 lib/src/ 下已有实现重名
lib/battery_plus_platform_interface.dart
lib/battery_plus_web.dart
ios/battery_plus/Sources/battery_plus/BatteryPlusPlugin.swift
macos/.../BatteryPlusPlugin.swift
windows/battery_plus_plugin.h windows/test/ linux/
android/.gitignore android/src/test/
test/battery_plus_test.dart test/battery_plus_method_channel_test.dart
example/integration_test/plugin_integration_test.dart
example/android/app/src/main/res/drawable-v21/ ...
这些是"默认工程"的重复文件,全部删除,只保留两个 ohos/ 目录和记录平台的 .metadata。原库的 lib/、test/、各平台实现一个字节都没动。
还有一处必要的改动:示例的 pubspec.yaml。
# 改前
battery_plus: ^7.1.1
# 改后(monorepo 内要指向本地适配版)
battery_plus:
path: ../
# 并移除 dev_dependencies 里的 flutter_driver / integration_test
integration_test 和 flutter_driver 这两个 SDK 依赖会让鸿蒙构建报 The srcPath is not a relative path(PUB_CACHE 与工程跨盘符时触发),示例不跑 OHOS 侧集成测试,故移除。
5️⃣ 补全适配仓库所需文件
新增了 README.OpenHarmony_CN.md 和 README.OpenHarmony.md(安装方式、能力对照、三个关键点、已知限制、验证结果、协议),并在包原有的 README.md 顶部加了一段 OHOS 指引。上游原有文档内容一字未改。
6️⃣ 代码推送
git add packages/battery_plus/battery_plus/ohos \
packages/battery_plus/battery_plus/example/ohos \
packages/battery_plus/battery_plus/{pubspec.yaml,README.md,README.OpenHarmony*.md,.metadata} \
packages/battery_plus/battery_plus/example/{pubspec.yaml,.metadata}
git commit -m "feat: 新增OpenHarmony平台实现"
git push origin feat/ohos_battery_plus_7.1.1
git push origin main
git tag -a 7.1.1-ohos-1.0.0-beta.1 -m "battery_plus 7.1.1 OpenHarmony 适配"
git push origin 7.1.1-ohos-1.0.0-beta.1
推送前必做的一件事:清空 example/ohos/build-profile.json5 里的 signingConfigs。签名配置里是明文密码,入库等于泄露。
二、上游给了什么契约
Dart 层全部能力都在 packages/battery_plus/battery_plus_platform_interface/lib/method_channel_battery_plus.dart:
MethodChannel methodChannel = const MethodChannel('dev.fluttercommunity.plus/battery');
EventChannel eventChannel = const EventChannel('dev.fluttercommunity.plus/charging');
Future<int> get batteryLevel => methodChannel.invokeMethod<int>('getBatteryLevel');
Future<bool> get isInBatterySaveMode => methodChannel.invokeMethod<bool>('isInBatterySaveMode');
Future<BatteryState> get batteryState =>
methodChannel.invokeMethod<String>('getBatteryState').then(parseBatteryState);
| Dart 侧 | 通道 | 方法 | 返回 |
|---|---|---|---|
batteryLevel | MethodChannel | getBatteryLevel | int(百分比) |
batteryState | MethodChannel | getBatteryState | String |
isInBatterySaveMode | MethodChannel | isInBatterySaveMode | bool |
onBatteryStateChanged | EventChannel | — | String |
状态字符串的解析在 lib/src/utils.dart:
BatteryState parseBatteryState(String state) {
switch (state) {
case 'full': return BatteryState.full;
case 'charging': return BatteryState.charging;
case 'discharging': return BatteryState.discharging;
case 'connected_not_charging': return BatteryState.connectedNotCharging;
case 'unknown': return BatteryState.unknown;
default:
throw ArgumentError('$state is not a valid BatteryState.');
}
}
这里的 default 分支是 throw,不是静默兜底。 也就是说原生侧拼错一个字符串,Dart 侧立刻抛异常——这点和很多"宽容处理"的插件正相反,反而是好事:错了会立刻暴露。
另外注意 Android 侧的实现细节:onListen 里除了注册广播接收器,还会立刻推一次当前状态:
override fun onListen(arguments: Any?, events: EventSink) {
chargingStateChangeReceiver = createChargingStateChangeReceiver(events)
ContextCompat.registerReceiver(it, chargingStateChangeReceiver, IntentFilter(ACTION_BATTERY_CHANGED), ...)
val status = getBatteryStatus()
publishBatteryStatus(events, status) // 订阅即刻推一次
}
鸿蒙侧要复刻这个行为,否则界面在第一次真实变化之前会一直空着。
三、鸿蒙侧的 API 选型:三个关键点
关键点一:@ohos.batteryInfo 是静态快照,根本没有事件接口
翻 @ohos.batteryInfo.d.ts 会看到一个很短的模块:batterySOC、chargingStatus、healthStatus、pluggedType、voltage、technology、batteryTemperature、isBatteryPresent、batteryCapacityLevel、nowCurrent —— 全部是 const,整个命名空间里没有任何 on() 订阅接口。
也就是说:查询三件事都能做,但"变化监听"这条路在这个模块里是不存在的。继续在 batteryInfo 上找 on() 只会浪费时间。
鸿蒙上电池变化的正规来源是系统公共事件。batteryInfo 里定义了一个 CommonEventBatteryChangedKey 枚举(EXTRA_SOC = 'soc'、EXTRA_CHARGE_STATE = 'chargeState' …),注释写得很直白:
Enumerates keys for querying the additional information about the COMMON_EVENT_BATTERY_CHANGED event.
这等于官方在提示:想监听变化,去订阅 usual.event.BATTERY_CHANGED。用 @ohos.commonEventManager:
const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
events: [commonEventManager.Support.COMMON_EVENT_BATTERY_CHANGED],
};
const subscriber = commonEventManager.createSubscriberSync(subscribeInfo);
commonEventManager.subscribe(subscriber,
(error: BusinessError, data: commonEventManager.CommonEventData): void => {
if (error) {
Log.e(TAG, `battery changed event error code=${error.code} message=${error.message}`);
return;
}
this.publishBatteryState();
});
两点说明:
commonEventManager对第三方应用可用,没有@systemapi限制(createSubscriber/subscribe自 API 9 起开放);@ohos.commonEvent命名空间下的同名常量已废弃,注释里明确写了@useinstead @ohos.commonEventManager:...Support#COMMON_EVENT_BATTERY_CHANGED,要用commonEventManager.Support里的那套。
事件本身只当作"变了"的信号,不解析它的 parameters,而是重新读一次 batteryInfo 快照再判定:
private publishBatteryState(): void {
const sink = this.sink;
if (sink === null) {
return;
}
const state: string = BatteryPlusPlugin.toStateString(batteryInfo.chargingStatus);
Log.i(TAG, `battery state changed -> ${state}`);
sink.success(state);
}
这样做的代价是可能重复推同一个值(BATTERY_CHANGED 在电量变化时也会触发),好处是状态判定只有一条代码路径,不会出现"事件里的值和快照不一致"这种分叉。上游 Android 也是同样的取舍。
关键点二:省电模式不能写成 !== MODE_NORMAL
isInBatterySaveMode 用 @ohos.power:
function getPowerMode(): DevicePowerMode;
DevicePowerMode 的取值是:
MODE_NORMAL = 600
MODE_POWER_SAVE
MODE_PERFORMANCE
MODE_EXTREME_POWER_SAVE
MODE_CUSTOM_POWER_SAVE = 650
直觉写法 mode !== MODE_NORMAL 是错的——里面还有一个 MODE_PERFORMANCE(性能模式),它不是省电模式。正确写法是只认三种省电:
return mode === power.DevicePowerMode.MODE_POWER_SAVE
|| mode === power.DevicePowerMode.MODE_EXTREME_POWER_SAVE
|| mode === power.DevicePowerMode.MODE_CUSTOM_POWER_SAVE;
getPowerMode() 不需要申请权限(@ohos.power 里带 @permission ohos.permission.REBOOT 的是重启相关接口,不是它)。
关键点三:充电状态要逐一对应,别自己发明
BatteryChargeState 只有四个值,和 Android 的五个状态不是一一对齐的,映射关系固定下来是这样:
OHOS BatteryChargeState | 上报字符串 | 对应 Android 状态 |
|---|---|---|
ENABLE(正在充电) | charging | BATTERY_STATUS_CHARGING |
FULL(已充满) | full | BATTERY_STATUS_FULL |
DISABLE(接了电源但未充电) | connected_not_charging | BATTERY_STATUS_NOT_CHARGING |
NONE(未充电) | discharging | BATTERY_STATUS_DISCHARGING |
| 其它 | unknown | BATTERY_STATUS_UNKNOWN |
DISABLE → connected_not_charging 这一条最容易写错:它不是"没插电",而是"插了电但没充"。
电量本身反而简单,batterySOC 直接就是 0–100 的整数百分比,取整即可。唯一要对齐的是失败语义——Android 取不到电量时返回 -1,插件把 -1 转成 error("UNAVAILABLE"),鸿蒙侧照做:
const level: number = batteryInfo.batterySOC;
if (level === undefined || level === null || level < 0 || level > 100) {
result.error('UNAVAILABLE', 'Battery level not available.', null);
return;
}
result.success(Math.floor(level));
四、生命周期
公共事件订阅是挂在系统服务上的,不注销就会残留。所以 onDetachedFromEngine 第一件事是退订:
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.unsubscribeBatteryChanged();
this.sink = null;
this.channel?.setMethodCallHandler(null);
this.channel = null;
this.eventChannel = null;
}
onCancel 同理——Dart 侧取消订阅时必须退订:
onCancel(args: Object | null): void {
this.sink = null;
this.unsubscribeBatteryChanged();
}
订阅侧加了一道 subscriber !== null 的幂等守卫,避免热重载或重复 onListen 时重复订阅(onListen 在热重启时可能被连续调用两次)。
事件通道的 onListen 里,订阅之后立刻推一次当前状态,复刻上游 Android 的行为:
onListen(args: Object | null, events: EventSink): void {
this.sink = events;
this.subscribeBatteryChanged();
this.publishBatteryState(); // 与上游 Android 的 onListen 一致
}
顺带说明一点:EventChannel.setStreamHandler() 的类型签名不接受 null,所以插件分离时只把引用丢掉,不去反注册 handler——引擎都已经分离,没必要。
五、demo 工程
如何引用适配库
本仓库是 monorepo,包在 packages/battery_plus/battery_plus,所以下游用 git 引用时必须带 path:
dependencies:
battery_plus:
git:
url: https://atomgit.com/oh-flutter/battery_plus.git
ref: 7.1.1-ohos-1.0.0-beta.1
path: packages/battery_plus/battery_plus
少写 path 时 pub 会在仓库根找 pubspec.yaml,而根上那个是 monorepo 的聚合描述,会直接报找不到包。示例工程内部则用相对路径依赖本适配版:
battery_plus:
path: ../
真实使用案例
示例页(example/lib/main.dart,上游自带、未改逻辑)做了三件事,正好覆盖三条通道:
void initState() {
super.initState();
_battery.batteryState.then(_updateBatteryState); // MethodChannel 查询
_batteryStateSubscription =
_battery.onBatteryStateChanged.listen(_updateBatteryState); // EventChannel 监听
}
界面上是一个状态文字 + 两个按钮:「Get battery level」弹窗显示电量百分比,「Is in Battery Save mode?」弹窗显示布尔值。
编译构建与运行效果
cd packages/battery_plus/battery_plus/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 io.flutter.plugins.batteryexample.battery_plus_example
六、真机验证
验证在 HarmonyOS 7.0.0(26.0.0) Beta2 的 API 26 模拟器(ohos-x64)上完成。鸿蒙模拟器支持直接注入电量与充电状态,所以这个库不用改真机就能完整验证。
电量读取:注入 25% 后点「Get battery level」。
devecocli emulator battery --target "Pura X View" --level 25

充电状态与事件通道:注入充电状态后,事件推来新值,界面文字同步变化。
devecocli emulator battery --target "Pura X View" --status charging

省电模式:点「Is in Battery Save mode?」,返回 false。
原生日志把整条链路记了下来:
FlutterEngineCxnRegistry --> Adding plugin: BatteryPlusPlugin
BatteryPlusPlugin --> subscribed COMMON_EVENT_BATTERY_CHANGED
BatteryPlusPlugin --> battery state changed -> discharging
BatteryPlusPlugin --> battery state changed -> discharging ← 注入 25% 触发
BatteryPlusPlugin --> battery state changed -> charging ← 注入充电状态触发
这里要强调一下这次验证的含金量。 日志里后两条不是"订阅时推的初始值",而是注入操作真实触发了系统公共事件:说明 commonEventManager 的订阅确实生效,事件也确实穿透到 Dart 层驱动了界面刷新。很多库在模拟器上只能验证到"通道通、初始值对",而这次连"真实变化"这一环也验到了。
一个小坑:
devecocli emulator battery的--level与--status不能同时指定,会报Only one operation option can be specified.,得分两次调用。
七、已知限制
batteryLevel取不到时返回错误而不是 0。batterySOC越界时按上游 Android 约定返回error("UNAVAILABLE", "Battery level not available."),Dart 侧收到的是PlatformException。isInBatterySaveMode读失败时返回false。getPowerMode()抛错时按"非省电"处理并打日志,不向 Dart 抛异常。- 订阅的是
BATTERY_CHANGED,不是"充电状态变化"专有事件。电量或充电状态任一变化都会触发,所以事件比严格需要更频繁;插件每次重读快照,不会丢状态,但可能重复推同一个值(上游 Android 同样如此)。 flutter_driver/integration_test已从示例的 dev_dependencies 移除,原因是它们会让鸿蒙构建报The srcPath is not a relative path。
八、常见问题
Q:为什么不用 batteryInfo.on(...) 监听变化?
A:因为不存在。@ohos.batteryInfo 整个命名空间里只有 const 快照,没有任何 on() 订阅接口。翻遍 .d.ts 也找不到——只能改用公共事件 usual.event.BATTERY_CHANGED。
Q:usual.event.BATTERY_CHANGED 第三方应用能订阅吗?
A:能。commonEventManager 的 createSubscriber / subscribe 自 API 9 起对第三方开放,没有 @systemapi 限制,也不需要额外权限。本次实测订阅成功并在注入电量后收到了事件。
Q:为什么事件里明明带了 soc / chargeState,还要重读快照?
A:为了让状态判定只有一条代码路径。如果事件用 parameters 解析、查询用 batteryInfo 读取,两处逻辑就要各自维护一份映射,一旦不一致就会很难查。重读的代价只是一次同步取值。
Q:省电模式为什么不能写成 mode !== MODE_NORMAL?
A:因为 DevicePowerMode 里有 MODE_PERFORMANCE(性能模式)。写成不等于正常模式,会把性能模式误报成省电模式。只有 MODE_POWER_SAVE / MODE_EXTREME_POWER_SAVE / MODE_CUSTOM_POWER_SAVE 三种才算省电。
Q:状态字符串拼错会怎样?
A:Dart 侧直接抛 ArgumentError: xxx is not a valid BatteryState.。上游 parseBatteryState() 的 default 分支是 throw,不是静默兜底——所以拼错一定会暴露,不会悄悄变成 unknown。
Q:DISABLE 为什么映射成 connected_not_charging 而不是 discharging?
A:DISABLE 的语义是"接了电源但没有在充电",对应 Android 的 BATTERY_STATUS_NOT_CHARGING;真正的"放电中"是 NONE。
Q:为什么示例的 pubspec.yaml 要改成 path: ../?
A:monorepo 里上游写的是 battery_plus: ^7.1.1(指向 pub.dev),本地适配版必须用路径依赖才能在示例里生效。上游 monorepo 平时靠 melos bootstrap 生成覆盖,这里直接写死路径更直观。
Q:这个库需要申请权限吗?
A:不需要。电量、充电状态、省电模式、公共事件订阅都不涉及权限,module.json5 里不用声明 requestPermissions。
小结
这个库的逻辑量不大,但把一类典型问题讲透了:同一个业务概念在鸿蒙上可能被拆成"快照 + 公共事件"两套机制。查询用 batteryInfo,监听用 commonEventManager,两者名字都不一样,光看模块名是猜不到的。
另外两个坑也值得记住:DevicePowerMode 里混着一个不是省电的 MODE_PERFORMANCE;上游 Dart 侧的状态解析是 throw 而不是兜底,字符串必须逐字对齐。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | battery_plus(上游 7.1.1 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-flutter/battery_plus |
| 适配 TAG | 7.1.1-ohos-1.0.0-beta.1 |
| 适配分支 | feat/ohos_battery_plus_7.1.1 |
| 包路径 | packages/battery_plus/battery_plus(monorepo,引用时需带 path) |
dependencies:
battery_plus:
git:
url: https://atomgit.com/oh-flutter/battery_plus.git
ref: 7.1.1-ohos-1.0.0-beta.1
path: packages/battery_plus/battery_plus
验证环境
| 项 | 版本 |
|---|---|
| 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 battery |
欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
Flutter 三方库鸿蒙适配清单:https://atomgit.com/oh-flutter/flutter-ohos-adaptation-checklist
更多推荐

所有评论(0)