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 侧通道方法返回
batteryLevelMethodChannelgetBatteryLevelint(百分比)
batteryStateMethodChannelgetBatteryStateString
isInBatterySaveModeMethodChannelisInBatterySaveModebool
onBatteryStateChangedEventChannel—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(正在充电)chargingBATTERY_STATUS_CHARGING
FULL(已充满)fullBATTERY_STATUS_FULL
DISABLE(接了电源但未充电)connected_not_chargingBATTERY_STATUS_NOT_CHARGING
NONE(未充电)dischargingBATTERY_STATUS_DISCHARGING
其它unknownBATTERY_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
适配 TAG7.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 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 battery

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

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

Logo

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

更多推荐