环境搭建指引:https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/ohos/getting-started/flutter-oh-env-setup.md

app_badge_plus 只做一件事:给应用图标加角标数字(0 表示清除)。它的 1.3.5 版支持 Android、iOS、macOS,没有 OpenHarmony。

选它做适配,是因为它在安卓上的实现方式很有代表性:角标是桌面(启动器)的私有能力,于是上游不得不写一整套适配层——Apex / Asus / HTC / Huawei / Hihonor / LG / MiUI / Nexus / Nowa / OPPO / Samsung / Sony / Vivo / Yandex / ZTE 等近 20 个厂商分支,各用各的私有广播或 content:// provider。而鸿蒙把这件事做成了系统统一接口,适配层直接塌缩成一次调用——但代价是语义变了:鸿蒙的角标是通知角标,通知开关关着就不显示。这一条如果不处理,isSupported() 就会给出错误答案。

适配对象:上游 app_badge_plus 1.3.5(MIT);适配产物 TAG 1.3.5-ohos-1.0.0-beta.1。


一、这个库要解决什么

1.1 上游 API

只有两个静态方法:

// 设置角标;0 表示清除
await AppBadgePlus.updateBadge(5);
await AppBadgePlus.updateBadge(0);

// 当前设备/桌面是否支持角标
final bool supported = await AppBadgePlus.isSupported();

example 里的用法也很直白:FloatingActionButton 每点一次 count += 1 再 updateBadge(count)。

1.2 契约

class MethodChannelAppBadgePlus extends AppBadgePlusPlatform {
  final methodChannel = const MethodChannel('app_badge_plus');

  
  Future<void> updateBadge(int count) async {
    await methodChannel.invokeMethod<void>('updateBadge', {'count': count});
  }

  
  Future<bool> isSupported() async {
    return (await methodChannel.invokeMethod<bool>('isSupported')) ?? false;
  }
}

一条方法通道、两个方法、一个参数,Dart 层没有任何平台门(app_badge_plus.dart 就是把调用转给平台接口实例),pubspec.yaml 里也只声明了三个平台:

flutter:
  plugin:
    platforms:
      android: { package: me.liolin.app_badge_plus, pluginClass: AppBadgePlusPlugin }
      ios:     { pluginClass: AppBadgePlusPlugin }
      macos:   { pluginClass: AppBadgePlusPlugin }

鸿蒙不在列表里 → 平台接口保持默认的 MethodChannelAppBadgePlus → 只要原生把 app_badge_plus 这条通道接住即可,Dart 一个字都不用改。

1.3 一个容易忽略的基线问题:master 的版本号落后于发布版

克隆下来先对基线,第一眼是"版本不一致":

git clone https://gh-proxy.com/https://github.com/windows7lake/app_badge_plus.git
# HEAD = 69511cf   pubspec version: 1.3.4
# pub.dev 最新发布版:1.3.5

看起来像是"仓库落后一个版本",于是逐文件比对(按 LF 归一化后比内容哈希):

node .agents/tools/tree-diff.mjs _probe/cand11/app_badge_plus _probe/abp_work
相同: 137  内容不同: 2  仅 A 有: 0  仅 B 有: 10
仅 B 有(仓库有、发布包没有): .gitignore / .metadata / android\.gitignore / example\**\.gitignore ...
内容不同:
  CHANGELOG.md
  pubspec.yaml

代码完全一致,只有版本号与 CHANGELOG 不同——也就是说 1.3.5 就是 master 的代码、只是发布时改了版本号(上游的 release 提交没有回到 master)。结论:基线取 1.3.5,同时把仓库里的 version 对齐到 1.3.5(否则 TAG 与 pub.dev 上的版本号对不上)。


二、选库:四道筛 + 在线查重

2.1 四筛

筛子检查结果
① pub.dev 平台列表是否已含 ohos[android, ios, macos],不含 → 需要适配
② 兄弟包上游根目录有没有 <lib>_ohos;pub.dev 上有没有 app_badge_plus_ohos都没有 → 需要自己写
③ Dart 平台门有没有 Platform.is* / defaultTargetPlatform 分支 / UnsupportedError无 → 可适配
④ 依赖体检node .agents/tools/dep-ohos-check.mjs app_badge_plusdeps ok: plugin_platform_interface(pure) → 干净

2.2 在线查重:403 不等于"没人适配"

四个组织的实时探测结果里,hxa-flutter 对一个不存在的仓库名返回了 403:

=== app_badge_plus ===
  verdict      : CHECK-MANUALLY
  pub.dev      : v1.3.5 platforms=[android,ios,macos]
  atomgit      : hxa-flutter/app_badge_plus -> status=403

403 什么都不能说明(本会话实测过:同一接口下"确定存在"的仓库返回 200、"确定不存在"的随机名返回 404、而这个 403 既不是存在也不是不存在)。不能靠再撞一次接口来消解,要用不同形态的接口交叉验证——拉该组织的全量仓库列表再判:

node .agents/tools/org-repos.mjs _probe/org-repos.json      # 四组织全量快照:223 / 375 / 123 / 110

再把候选名与快照里的 831 个仓库做精确匹配(含 fluttertpc_<名> / flutter_<名> / <名>_ohos 三种变体):

---- app_badge_plus

干净,可以适配。(同一轮里被这条规则排除掉的有 ambient_light(hxa 已适配)、local_auth(pub.dev 上已有 local_auth_ohos)、app_settings、device_apps、is_lock_screen、volume_listener。)


三、六步适配流程

第一步:把上游同步到 AtomGit

node .agents/tools/atomgit.mjs create oh-flutter app_badge_plus "app_badge_plus 的 OpenHarmony 适配(应用角标)"

第二步:本地克隆(并用发布版核对基线)

git clone https://gh-proxy.com/https://github.com/windows7lake/app_badge_plus.git _probe/abp_work

工作区按序号放好,本篇对应 _probe/abp_work/。

第三步:建分支并补出鸿蒙目录

cd _probe/abp_work
git checkout -b feat/ohos_app_badge_plus_1.3.5
flutter create -t plugin --platforms ohos .

pubspec.yaml 补一行,并把版本号对齐到发布版:

version: 1.3.5
flutter:
  plugin:
    platforms:
      # ... 三个原平台
      ohos:
        pluginClass: AppBadgePlusPlugin

第四步:写鸿蒙实现

只新增一个文件:ohos/src/main/ets/components/plugin/AppBadgePlusPlugin.ets(外加宿主侧的授权请求,见 4.2)。

第五步:补全额外文件

ohos/oh-package.json5 改成 version: 1.3.5 / license: MIT;新增 README.OpenHarmony.md、README.OpenHarmony_CN.md、CHANGELOG.OpenHarmony.md;根 README.md 的支持平台列表加 OpenHarmony;example/lib/main.dart 改成自检台。

第六步:推送并打 TAG

git push atomgit feat/ohos_app_badge_plus_1.3.5
git push atomgit HEAD:main
git tag -a 1.3.5-ohos-1.0.0-beta.1 -m "app_badge_plus 1.3.5 OpenHarmony 适配 1.0.0-beta.1"
git push atomgit 1.3.5-ohos-1.0.0-beta.1

提交前必须清空 example/ohos/build-profile.json5 里的 signingConfigs(devecocli signature generate 会把证书路径与明文密码写进去)。

在这里插入图片描述


四、代码写在哪个文件

ohos/
├── index.ets                                   # export { default } from './src/main/ets/components/plugin/AppBadgePlusPlugin'
├── oh-package.json5                            # name: app_badge_plus, version: 1.3.5, license: MIT
├── build-profile.json5 / hvigorfile.ts         # HAR 模板原文
└── src/main/
    ├── module.json5                            # { name: app_badge_plus, type: har }
    └── ets/components/plugin/
        └── AppBadgePlusPlugin.ets              # 本篇唯一新增的实现文件
example/ohos/entry/src/main/ets/entryability/
    └── EntryAbility.ets                        # 宿主:请求通知授权(见 4.2)

4.1 二十个厂商分支 → 一次系统调用

先看上游安卓的规模:android/src/main/kotlin/me/liolin/app_badge_plus/impl/ 下面是 ApexLauncherBadge / AsusLauncherBadge / HihonorLauncherBadge / HtcLauncherBadge / HuaweiLauncherBadge / LGLauncherBadge / MiUIBadge / NexusLauncherBadge / NowaLauncherBadge / OPPOLauncherBadge / SamsungLauncherBadge / SonyLauncherBadge / VivoLauncherBadge / YandexLauncherBadge / ZTELauncherBadge 等近 20 个实现类,每家在 AndroidManifest.xml 里还要各自申请 com.sec.android.provider.badge.permission.WRITE、com.huawei.android.launcher.permission.CHANGE_BADGE 之类的私有权限。

鸿蒙这边是系统能力(@ohos.notificationManager,API 10 起、无需权限声明):

await notificationManager.setBadgeNumber(badge);        // 0 = 清除;>99 由系统显示为 "99+"
const readback: number = await notificationManager.getBadgeNumber();

不需要按设备型号分支,也不需要任何权限声明——"厂商适配层"这一整层在鸿蒙上不存在。

4.2 语义差异:鸿蒙的角标是通知角标

这是本库在鸿蒙上最容易做错的地方。安卓的 isSupported() 问的是"当前桌面支不支持角标";鸿蒙的角标挂在通知体系上——应用的通知总开关关着,角标就不显示。所以鸿蒙实现把两个条件都查了:

private async isSupported(): Promise<boolean> {
  const enabled: boolean = await notificationManager.isNotificationEnabled();
  const setting = await notificationManager.getNotificationSetting();
  const badgeEnabled: boolean = setting.badgeNumberEnabled ?? true;
  Log.i(TAG, `isSupported: notificationEnabled=${enabled} badgeNumberEnabled=${setting.badgeNumberEnabled}`);
  return enabled && badgeEnabled;
}

实测(新装应用、尚未授权通知):

AppBadgePlusPlugin --> isSupported: notificationEnabled=false badgeNumberEnabled=true

→ isSupported() = false。这个 false 是对的,不是接口没接通:此时确实显示不出角标。

而新装应用的通知开关默认是关的,必须先由用户在系统弹窗里允许。这个弹窗只能由拿着 UIAbilityContext 的宿主请求(插件够不着 UIAbility),所以在示例的 EntryAbility 里加:

onWindowStageCreate(windowStage: window.WindowStage): void {
  super.onWindowStageCreate(windowStage);
  notificationManager.isNotificationEnabled().then((enabled: boolean) => {
    Log.i(TAG, `notification enabled before request = ${enabled}`);
    if (enabled) return;
    notificationManager.requestEnableNotification(this.context)
      .then(() => Log.i(TAG, 'requestEnableNotification resolved (user allowed)'))
      .catch((error: BusinessError) => Log.e(TAG, `failed code=${error.code} ${error.message}`));
  });
}

这与上游 README 里"Android 13+ 要先申请通知权限再设角标"是同一件事,只是位置在宿主。文档还明确:用户拒绝后本 API 不会再弹(只能引导去系统设置),所以示例只在启动时请求一次。

4.3 回读为什么必须做两次

Dart API 只有写没有读,插件把系统回读值打进 hilog,设备侧才有客观证据。但第一版只做了一次回读,结果抓到一个反直觉的现象:

setBadgeNumber(120) -> getBadgeNumber=120 (系统会显示为 99+)
setBadgeNumber(0)   -> getBadgeNumber=120      <-- 清除了,却回读成 120

而此刻桌面上角标已经消失了(截图可证)。也就是说系统侧角标状态是异步落库的,紧接着读会拿到旧值。于是实现改成两次回读:

await notificationManager.setBadgeNumber(badge);
const immediate: number = await notificationManager.getBadgeNumber();
Log.i(TAG, `setBadgeNumber(${badge}) -> getBadgeNumber=${immediate}`);
setTimeout(async (): Promise<void> => {
  const settled: number = await notificationManager.getBadgeNumber();
  Log.i(TAG, `badge settled: set=${badge} immediate=${immediate} settled=${settled}`);
}, 300);

第一行证明"调用被受理",badge settled 行才是最终状态。这条经验对适配其它"写系统设置"的库同样适用:回读要带 settle 延迟,否则会把异步落库当成失败。

4.4 这次 flutter create 生成的模板垃圾

在一个"已有 SPM 结构 iOS/macOS 源码"的插件上跑 flutter create -t plugin --platforms ohos .,会多出这些东西(全部要删,且只删真正多余的):

android/build.gradle.kts / android/settings.gradle.kts          # 上游是 build.gradle(Groovy)
example/android/build.gradle.kts / app/build.gradle.kts / settings.gradle.kts
example/integration_test/                                        # 模板用例,与上游 API 不符
example/ios/Runner/SceneDelegate.swift
ios/app_badge_plus/Sources/   macos/app_badge_plus/Sources/       # 模板的 SPM 布局(复数 Sources)

这里踩了一个坑:上游的 iOS/macOS 源码目录是单数 Source/,模板生成的是复数 Sources/。我一开始按"整个目录都是模板垃圾"把 ios/app_badge_plus 与 macos/app_badge_plus 整个删掉,连带删掉了上游被跟踪的 Package.swift 与 Source/app_badge_plus/*.swift——git status 立刻冒出一片 D。修法是 git checkout -- ios macos 还原,然后只删模板新增的那个目录。教训:清理模板垃圾前先 git ls-files <目录> 分清"上游本来就有的"和"模板刚生成的"。


五、真机(模拟器)验证

示例被改造成"自检台":顶部显示 isSupported(),六个按钮覆盖"加一 / 减一 / 清零 / 设 5 / 设 120 / 重新查询",每次调用在界面与 [BADGE-CHECK] 日志里留痕,原生侧回读值打到 hilog。

项值
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1(Dart 3.12.2)
DevEco Studio26.0.0.621(OpenHarmony SDK API 26)
设备Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64
产物example/build/ohos/hap/entry-default-signed.hap
取日志hdc shell hilog -x | Select-String "AppBadgePlus"

5.1 逐项实测

操作设备侧结果
冷启动(通知未授权)AppBadgePlusExample --> notification enabled before request = false,同时系统弹出"允许 app_badge_plus_example 向你发送通知?";界面显示 isSupported:false
点"允许"后重新查询isSupported: notificationEnabled=true badgeNumberEnabled=true,界面变 isSupported:true(可显示角标)
updateBadge(5)setBadgeNumber(5) -> getBadgeNumber=5;回桌面截图:图标右上角出现红色角标 “5”
updateBadge(120)setBadgeNumber(120) -> getBadgeNumber=120 (系统会显示为 99+);桌面角标显示 “99+”
updateBadge(0)立刻回读仍是旧值 120;桌面角标消失(badge settled 行给出最终值)
授权持久化模拟器重启后再启动应用,日志为 notification enabled before request = true,不再弹窗
Dart 层未被破坏flutter test → All tests passed!

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.2 关于验证环境的一点实情

本次验证期间模拟器崩过两次(宿主侧 code 3221226356 堆损坏,之后需要冷启动约 5 分钟才重新上线),其中一次正好发生在"设 120 → 清 0"之间。重连后 notification enabled before request = true 说明授权状态是持久的,也说明前面那张"99+"的截图来自崩溃之前、角标消失的截图来自崩溃之后——两条证据分别来自两个进程,反而更硬。


六、编译与构建踩坑

6.1 window.WindowStage 必须先导入 window

在宿主 EntryAbility 里写 onWindowStageCreate(windowStage: window.WindowStage) 会直接编译失败:

ERROR: 10505001 ArkTS Compiler Error
Error Message: 'window' only refers to a type, but is being used as a namespace here.
  At EntryAbility.ets:23:36

补一行 import { window } from '@kit.ArkUI'; 即可。

6.2 示例依赖会拦住鸿蒙构建

上游示例依赖 flutter_local_notifications(发通知)与 permission_handler(申请通知权限),这两个包在鸿蒙上没有实现。鸿蒙示例改成只调本插件 API + 读 hilog 的自检台,并把两个依赖从 example/pubspec.yaml 移除。连带影响:example/macos/Flutter/GeneratedPluginRegistrant.swift 会被 flutter pub get 重新生成(少注册 FlutterLocalNotificationsPlugin 两行),这是正确的连带改动,要一起提交。

6.3 模板自带的 widget test 会挡住 flutter analyze

示例改写后,模板生成的 example/test/widget_test.dart 还在引用上游的 MyApp:

error - The name 'MyApp' isn't a class - example\test\widget_test.dart:16:35

它断言的是上游那套界面,与自检台不匹配,因此删除(插件自身的 test/ 单测保留并全部通过)。


七、已知限制

  • 角标值大于 99 由系统显示为 “99+”(文档行为),插件不截断;
  • 通知开关关闭时不显示角标:isSupported() 会返回 false,此时 setBadgeNumber 仍会记录数字,但桌面不一定显示;
  • 通知授权弹窗只能弹一次:用户拒绝后 requestEnableNotification 不再弹,只能引导用户去系统设置;
  • 角标状态异步落库:调用后立刻回读可能拿到旧值(实测 setBadgeNumber(0) 后立刻回读仍是 120),所以实现里做了 300ms 延迟回读;
  • 示例移除了两个上游依赖(flutter_local_notifications / permission_handler),并删除了断言上游界面的 example/test/widget_test.dart;
  • 只实现"设数字"这一层:本库的 Dart API 里没有"读角标",鸿蒙实现了 getBadgeNumber() 但仅供日志自证,没有对外暴露新接口(保持 Dart 层与上游一致)。

八、常见问题

Q1:为什么基线取 1.3.5 而不是仓库 master 的 1.3.4?
两者代码逐文件一致(137 个文件相同,只有 pubspec.yaml 与 CHANGELOG.md 不同),1.3.5 就是发布出去的代码。基线取发布版、TAG 也按 1.3.5 命名,用户对照 pub.dev 时不会困惑;仓库里的 version 一并对齐到 1.3.5。

Q2:isSupported() 为什么返回 false?设备不支持吗?
不是。鸿蒙的角标是通知角标,新装应用的通知开关默认关闭,此时确实显示不出角标,返回 false 是如实的。先让宿主调 requestEnableNotification() 拿到用户授权,再查就是 true。

Q3:能不能不给通知权限就设角标?
可以调用,setBadgeNumber 不会因通知关闭而报错,但桌面不保证显示。角标属于通知体系,这一点与安卓"桌面私有能力"的模型不同。

Q4:为什么插件不自己弹通知授权弹窗?
弹窗需要 UIAbilityContext(requestEnableNotification(context),且要求 UI 已加载),而插件不持有 UIAbility。这与上游 README 让宿主先申请通知权限的要求是一致的——该由宿主做的事,插件不越权。

Q5:为什么日志里 getBadgeNumber() 和刚设的值不一致?
系统角标状态异步落库。实现里改成"立刻回读 + 300ms 后再回读",badge settled 那行才是最终值。

Q6:一次调用会不会触发两条日志?
会:setBadgeNumber(n) -> getBadgeNumber=x 与 300ms 后的 badge settled: set=n immediate=x settled=y。两条都留着是有意的——排查时能区分"没受理"和"受理了但还没落库"。

Q7:需要额外依赖或权限声明吗?
都不需要。鸿蒙侧只用 @kit.NotificationKit,setBadgeNumber / getBadgeNumber 无需在 module.json5 里声明权限。

Q8:updateBadge 传负数或超大值会怎样?
实现把 <= 0 归一到 0(清除),正数原样交给系统,超过 99 由系统显示 99+。上游安卓侧的语义是"非正数即清除",鸿蒙保持一致。

Q9:示例为什么不能直接用上游的 main.dart?
它依赖 flutter_local_notifications(发通知)与 permission_handler(申请权限),两者在鸿蒙上都没有实现,留着会让 flutter build hap 失败。自检台只调本插件 API,验证目标更集中。


九、本篇用到的库

项值
适配仓库https://atomgit.com/oh-flutter/app_badge_plus
上游仓库https://github.com/windows7lake/app_badge_plus
上游版本1.3.5(MIT;代码与 master 69511cf 逐文件一致,仅版本号/CHANGELOG 不同)
适配 TAG1.3.5-ohos-1.0.0-beta.1
适配分支feat/ohos_app_badge_plus_1.3.5
平台目录ohos/(插件 HAR)、example/ohos/(示例工程)
通道方法通道 app_badge_plus(updateBadge / isSupported)
鸿蒙侧依赖@kit.NotificationKit(notificationManager.setBadgeNumber / getBadgeNumber / isNotificationEnabled / getNotificationSetting / requestEnableNotification)

依赖写法(写死 TAG,不跟分支):

dependencies:
  app_badge_plus:
    git:
      url: https://atomgit.com/oh-flutter/app_badge_plus.git
      ref: 1.3.5-ohos-1.0.0-beta.1

验证环境

项值
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.0.0.621(OpenHarmony SDK API 26)
设备Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64
构建产物example/build/ohos/hap/entry-default-signed.hap

复现命令

# 1. 构建(PUB_CACHE 必须与工程同盘;模拟器是 ohos-x64;Log.i 只在 debug 下可见)
$env:PUB_CACHE = "E:\pub-cache"
cd _probe/abp_work/example/ohos
devecocli signature generate            # 首次需要;提交前记得清空 signingConfigs
cd ..
flutter pub get
flutter build hap --debug --target-platform ohos-x64

# 2. 安装并启动(包名来自 android package,注意不是 com.example.*)
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b me.liolin.app_badge_plus_example

# 3. 首次启动会弹"允许发送通知?",点"允许"后再点"重新查 isSupported"
#    然后依次点:设 5 → 回桌面截图 → 设 120 → 回桌面截图 → 清零 → 回桌面截图
hdc shell snapshot_display -f /data/local/tmp/abp.jpeg
hdc file recv /data/local/tmp/abp.jpeg .

# 4. 取原生日志
hdc shell hilog -x | Select-String "AppBadgePlus"

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐