flutter_app_icon_badge 插件鸿蒙适配:实现跨平台应用图标角标管理

2025-05-27 13:17:06
132次阅读
0个评论

flutter_app_icon_badge 插件鸿蒙适配:实现跨平台应用图标角标管理

本项目作者:坚果

您可以使用这个Flutter插件来更改应用程序图标上的角标

作者仓库:https://github.com/badver/flutter_app_icon_badge/

在数字化浪潮的推动下,跨平台开发框架如 Flutter 凭借其高效、便捷的特性,成为了开发者们的宠儿。而鸿蒙系统的崛起,更是为跨平台开发注入了新的活力。为了助力开发者在鸿蒙生态中快速实现 flutter_app_icon_badge更改应用程序图标上的角标功能,本文将深入浅出地为大家解析如何适配 flutter_app_icon_badge 三方库至鸿蒙平台。

一、适配鸿蒙版 flutter_app_icon_badge 三方库

(一)版本选择与仓库简介

我们先去 pub 上查看最新版本,我们选择以 0.0.10版本为基础进行适配。flutter_app_icon_badge 是一个用于在 Flutter 应用中更改应用程序图标上的角标功能,其 GitHub 仓库为https://github.com/badver/flutter_app_icon_badge/ ,我们的目标是将这个插件适配到鸿蒙平台。

(二)引入背景与使用场景

在 OpenHarmony 北向生态的发展过程中,许多已经适配了 Flutter 的厂商在接入 OpenHarmony 时,都希望能够继续使用 FlutterToast 来实现通知功能。因此,我们提供了这个适配方案,采用插件化的适配器模式,帮助生态伙伴快速实现产品化。

本方案适用于已经支持 Flutter 框架的设备在移植到 OpenHarmony 系统过程中,作为一个备选方案。

(三)使用文档与插件库使用

适配 OpenHarmony 平台的详细使用指导可以参考:Flutter使用指导文档

在项目中使用该插件库时,只需在 pubspec.yaml 文件的 dependencies 中新增如下配置:

dependencies:
  flutter_app_icon_badge:
    git:
      url: "https://gitcode.com/nutpi/flutter_app_icon_badge.git"
      path: ""

然后在项目根目录运行 flutter pub get,即可完成依赖添加

接下来是具体的适配过程。

二、适配过程详解

(一)准备工作

确保已经配置好了 Flutter 开发环境,具体可参考 Flutter 配置指南。同时,从 官方插件库 下载待适配的三方插件。本指导书, 以适配 flutter_app_icon_badge 为例

image-20250417200546042

(二)插件目录结构

下载并解压插件后,我们会看到以下目录结构:

  • lib :对接 Dart 端代码的入口,由此文件接收到参数后,通过 channel 将数据发送到原生端。
  • android :安卓端代码实现目录。
  • ios :iOS 原生端实现目录。
  • example :一个依赖于该插件的 Flutter 应用程序,用于说明如何使用它。
  • README.md :介绍包的文件。
  • CHANGELOG.md :记录每个版本中的更改。
  • LICENSE :包含软件包许可条款的文件。

(三)创建插件的鸿蒙模块

在插件目录下,打开 Terminal,执行以下命令来创建一个鸿蒙平台的 Flutter 模块:

flutter create . --org dev.badver.flutter_app_icon_badge --template=plugin --platforms=ohos

步骤:

  1. 用vscode/trae打开刚刚下载好的插件。

  2. 打开Terminal,cd到插件目录下。

  3. 执行命令flutter create . --org dev.badver.flutter_app_icon_badge --template=plugin --platforms=ohos 创建一个ohos平台的flutter模块。

第一个问题,修改sdk的版本,适配旧版本。

我们做好修改就好。

(四)在根目录下添加鸿蒙平台配置

在项目根目录的 pubspec.yaml 文件中,添加鸿蒙平台的相关配置:

name: flutter_app_icon_badge
description: Flutter app icon badge plugin to change the badge on the icon of your app.
version: 2.0.0
homepage: https://github.com/badver/flutter_app_icon_badge

environment:
  sdk: '>=2.12.0 <4.0.0'
  flutter: ">=2.3.0"

dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  plugin:
    platforms:
      android:
        package: dev.badver.flutter_app_icon_badge
        pluginClass: FlutterAppIconBadgePlugin
      ios:
        pluginClass: FlutterAppIconBadgePlugin
      linux:
        pluginClass: FlutterAppIconBadgePlugin
      macos:
        pluginClass: FlutterAppIconBadgePlugin
      windows:
        pluginClass: FlutterAppIconBadgePlugin
      ohos:
        package: dev.badver.flutter_app_icon_badge
        pluginClass: FlutterAppIconBadgePlugin

(五)编写鸿蒙插件的原生 ArkTS模块

1. 创建鸿蒙插件模块

使用 DevEco Studio 打开鸿蒙项目。

2. 修改相关配置文件

ohos 目录内的 oh-package.json5 文件中添加 libs/flutter.har 依赖,并创建 .gitignore 文件,添加以下内容以忽略 libs 目录:

/node_modules
/oh_modules
/local.properties
/.preview
/.idea
/build
/libs
*.har
/.cxx
/.test
/BuildProfile.ets
/oh-package-lock.json5

oh-package.json5 文件内容如下:

{
  "name": "flutter_app_icon_badge",
  "version": "1.0.0",
  "description": "Flutter app icon badge plugin to change the badge on the icon of your app",
  "main": "index.ets",
  "author": "nutpi",
  "license": "Apache-2.0",
  "dependencies": {
    "@ohos/flutter_ohos": "file:./har/flutter.har"
  }
}

ohos 目录下创建 index.ets 文件,导出配置:


import FlutterAppIconBadgePlugin from './src/main/ets/components/plugin/FlutterAppIconBadgePlugin';
export default FlutterAppIconBadgePlugin;

3. 编写 ETS 代码

文件结构和代码逻辑可以参考安卓或 iOS 的实现,鸿蒙的 API 文档可以参考 :https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-notificationmanager

ohos的api可以参考:https://gitcode.com/openharmony/docs

以下是 FlutterAppIconBadgePlugin.ets 文件的代码示例:

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import { BusinessError } from '@kit.BasicServicesKit';
import { notificationManager } from '@kit.NotificationKit';

/** FlutterAppIconBadgePlugin **/
export default class FlutterAppIconBadgePlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;

  constructor() {
  }

  getUniqueClassName(): string {
    return "FlutterAppIconBadgePlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_app_icon_badge");
    this.channel.setMethodCallHandler(this)
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method == "updateBadge") {
      try {
        // 获取参数
        const countArg :string= call.argument("count");

        if (countArg === null || countArg === undefined) {
          console.error('Badge count argument is missing.');
          result.error("MISSING_ARGUMENT", "Badge count argument is required.", null);
          return;
        }

        // 确保 countArg 是数字类型
        let badgeNumber: number = Number(countArg);

        if (isNaN(badgeNumber)) {
          console.error('Invalid badge number received:', countArg);
          result.error("INVALID_ARGUMENT", "Badge count must be a number.", null);
          return;
        }

        notificationManager.setBadgeNumber(badgeNumber).then(() => {
          console.info(`Succeeded in setting badge number to ${badgeNumber}.`);
          result.success(null);
        }).catch((err: BusinessError) => {
          console.error(`Failed to set badge number. Code is ${err.code}, message is ${err.message}`);
          result.error(err.code.toString(), err.message, null);
        });
      } catch (e) {
        const error = e as Error;
        console.error(`Error processing updateBadge: ${error.message}`);
        result.error("UNKNOWN_ERROR", error.message, null);
      }
    }
    else if (call.method == "removeBadge") {
      try {
        notificationManager.setBadgeNumber(0).then(() => {
          console.info(`Succeeded in removing badge number.`);
          result.success(null);
        }).catch((err: BusinessError) => {
          console.error(`Failed to remove badge number. Code is ${err.code}, message is ${err.message}`);
          result.error(err.code.toString(), err.message, null);
        });
      } catch (e) {
        const error = e as Error;
        console.error(`Error processing removeBadge: ${error.message}`);
        result.error("UNKNOWN_ERROR", error.message, null);
      }
    } else if (call.method == "isAppBadgeSupported") {
      // 鸿蒙平台默认支持角标
      result.success(true);
    } else {
      result.notImplemented();
    }
  }
}

这里我主要参考的是

三、NotificationManager模块

本模块提供通知管理的能力,包括发布、取消发布通知,创建、获取、移除通知渠道,获取通知的使能状态、角标使能状态,获取通知的相关信息等。

1.导入

import { notificationManager } from '@kit.NotificationKit';

2.notificationManager.setBadgeNumber

setBadgeNumber(badgeNumber: number): Promise

设定角标个数,在应用的桌面图标上呈现。使用Promise异步回调。

当角标设定个数取值小于或等于0时,表示清除角标。取值大于99时,通知角标将显示99+。

系统能力:SystemCapability.Notification.Notification

参数:

参数名 类型 必填 说明
badgeNumber number 角标个数。

返回值:

类型 说明
Promise 无返回结果的Promise对象。

错误码:

以下错误码的详细介绍请参见通用错误码通知错误码

错误码ID 错误信息
401 Parameter error. Possible causes: 1. Mandatory parameters are left unspecified. 2. Incorrect parameter types. 3.Parameter verification failed.
1600001 Internal error.
1600002 Marshalling or unmarshalling error.
1600003 Failed to connect to the service.
1600012 No memory space.

示例:

import { BusinessError } from '@kit.BasicServicesKit';

let badgeNumber: number = 10;
notificationManager.setBadgeNumber(badgeNumber).then(() => {
  console.info(`Succeeded in setting badge number.`);
}).catch((err: BusinessError) => {
  console.error(`Failed to set badge number. Code is ${err.code}, message is ${err.message}`);
});

3.dart端是这样定义的

import 'dart:async';

import 'package:flutter/services.dart';

class FlutterAppIconBadge {
  static const MethodChannel _channel =
      const MethodChannel('flutter_app_icon_badge');

  /// Change badge on app icon
  static Future<void> updateBadge(int count) async {
    await _channel.invokeMethod('updateBadge', {"count": count});
  }

  /// Remove badge on app icon
  static Future<void> removeBadge() async {
    await _channel.invokeMethod('removeBadge');
  }

  /// Check if app badge is supported
  static Future<bool> isAppBadgeSupported() async {
    bool? appBadgeSupported =
        await _channel.invokeMethod('isAppBadgeSupported');
    return appBadgeSupported ?? false;
  }

  /// Check if app window is focused.
  static Future<bool> isAppFocused() async {
    bool? isAppFocused = await _channel.invokeMethod('isAppFocused');
    return isAppFocused ?? false;
  }
}

4.ios侧处理

类似ios侧处理这边的信息,并返回

    switch call.method {
        case "updateBadge":
          if let args = call.arguments as? Dictionary<String, Any>,
            let count = args["count"] as? Int {
            UIApplication.shared.applicationIconBadgeNumber = count
            result(nil)
          } else {
            result(FlutterError.init(code: "bad args", message: nil, details: nil))
          }
        case "removeBadge":
          UIApplication.shared.applicationIconBadgeNumber = 0
          result(nil)
        case "isAppBadgeSupported":
          result(true)
        default:
          result(FlutterMethodNotImplemented)
    }

5.ArkTS处理图标

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import { BusinessError } from '@kit.BasicServicesKit';
import { notificationManager } from '@kit.NotificationKit';

/** FlutterAppIconBadgePlugin **/
export default class FlutterAppIconBadgePlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;

  constructor() {
  }

  getUniqueClassName(): string {
    return "FlutterAppIconBadgePlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_app_icon_badge");
    this.channel.setMethodCallHandler(this)
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method == "updateBadge") {
      try {
        // 获取参数
        const countArg :string= call.argument("count");

        if (countArg === null || countArg === undefined) {
          console.error('Badge count argument is missing.');
          result.error("MISSING_ARGUMENT", "Badge count argument is required.", null);
          return;
        }

        // 确保 countArg 是数字类型
        let badgeNumber: number = Number(countArg);

        if (isNaN(badgeNumber)) {
          console.error('Invalid badge number received:', countArg);
          result.error("INVALID_ARGUMENT", "Badge count must be a number.", null);
          return;
        }

        notificationManager.setBadgeNumber(badgeNumber).then(() => {
          console.info(`Succeeded in setting badge number to ${badgeNumber}.`);
          result.success(null);
        }).catch((err: BusinessError) => {
          console.error(`Failed to set badge number. Code is ${err.code}, message is ${err.message}`);
          result.error(err.code.toString(), err.message, null);
        });
      } catch (e) {
        const error = e as Error;
        console.error(`Error processing updateBadge: ${error.message}`);
        result.error("UNKNOWN_ERROR", error.message, null);
      }
    }
    else if (call.method == "removeBadge") {
      try {
        notificationManager.setBadgeNumber(0).then(() => {
          console.info(`Succeeded in removing badge number.`);
          result.success(null);
        }).catch((err: BusinessError) => {
          console.error(`Failed to remove badge number. Code is ${err.code}, message is ${err.message}`);
          result.error(err.code.toString(), err.message, null);
        });
      } catch (e) {
        const error = e as Error;
        console.error(`Error processing removeBadge: ${error.message}`);
        result.error("UNKNOWN_ERROR", error.message, null);
      }
    } else if (call.method == "isAppBadgeSupported") {
      // 鸿蒙平台默认支持角标
      result.success(true);
    } else {
      result.notImplemented();
    }
  }
}

6.优化通知

请求通知权限

应用需要获取用户授权才能发送通知。在通知发布前调用requestEnableNotification()方法,弹窗让用户选择是否允许发送通知,后续再次调用requestEnableNotification()方法时,则不再弹窗。

通知授权接口功能介绍

接口名 描述
isNotificationEnabled():Promise 查询通知是否授权。
requestEnableNotification(context: UIAbilityContext): Promise 请求发送通知的许可,第一次调用会弹窗让用户选择。

开发步骤

1.导入NotificationManager模块。

import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';

const TAG: string = '[PublishOperation]';
const DOMAIN_NUMBER: number = 0xFF00;

2.请求通知授权。

可通过requestEnableNotification的错误码判断用户是否授权。若返回的错误码为1600004,即为拒绝授权。

let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
notificationManager.isNotificationEnabled().then((data: boolean) => {
  hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data));
  if(!data){
    notificationManager.requestEnableNotification(context).then(() => {
      hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`);
    }).catch((err : BusinessError) => {
      if(1600004 == err.code){
        hilog.error(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`);
      } else {
        hilog.error(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
      }
    });
  }
}).catch((err : BusinessError) => {
    hilog.error(DOMAIN_NUMBER, TAG, `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`);
});

7.完整的代码

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import { BusinessError } from '@kit.BasicServicesKit';
import { notificationManager } from '@kit.NotificationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { AbilityAware, AbilityPluginBinding } from '@ohos/flutter_ohos';

const TAG: string = '[PublishOperation]';
const DOMAIN_NUMBER: number = 0xFF00;

/** FlutterAppIconBadgePlugin **/
export default class FlutterAppIconBadgePlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private static _context: common.UIAbilityContext | null = null;

  constructor() {
  }


  static get context(): common.Context | null {
    return FlutterAppIconBadgePlugin._context;
  }

  get context(): common.UIAbilityContext | null {
    return FlutterAppIconBadgePlugin._context;
  }

  getUniqueClassName(): string {
    return "FlutterAppIconBadgePlugin"
  }


  onAttachedToAbility(binding: AbilityPluginBinding): void {
    FlutterAppIconBadgePlugin._context = binding.getAbility().context;
    // Called when the plugin is attached to an Ability.
  }

  onDetachedFromAbility(): void {
    // this._uiContext = null;
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_app_icon_badge");
    this.channel.setMethodCallHandler(this)
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method == "updateBadge") {


      if (FlutterAppIconBadgePlugin._context) { // Add null check here
        try {
          notificationManager.isNotificationEnabled().then((data: boolean) => {
            hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data));

            try {
              // 获取参数
              const countArg: string = call.argument("count");

              if (countArg === null || countArg === undefined) {
                console.error('Badge count argument is missing.');
                result.error("MISSING_ARGUMENT", "Badge count argument is required.", null);
                return;
              }

              // 确保 countArg 是数字类型
              let badgeNumber: number = Number(countArg);

              if (isNaN(badgeNumber)) {
                console.error('Invalid badge number received:', countArg);
                result.error("INVALID_ARGUMENT", "Badge count must be a number.", null);
                return;
              }

              notificationManager.setBadgeNumber(badgeNumber).then(() => {
                console.info(`Succeeded in setting badge number to ${badgeNumber}.`);
                result.success(null);
              }).catch((err: BusinessError) => {
                console.error(`Failed to set badge number. Code is ${err.code}, message is ${err.message}`);
                result.error(err.code.toString(), err.message, null);
              });
            } catch (e) {
              const error = e as Error;
              console.error(`Error processing updateBadge: ${error.message}`);
              result.error("UNKNOWN_ERROR", error.message, null);
            }

            if (!data) {
              notificationManager.requestEnableNotification(FlutterAppIconBadgePlugin._context).then(() => {
                hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`);
              }).catch((err: BusinessError) => {
                if (1600004 == err.code) {
                  hilog.error(DOMAIN_NUMBER, TAG,
                    `[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`);
                } else {
                  hilog.error(DOMAIN_NUMBER, TAG,
                    `[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
                }
              });
            }
          }).catch((err: BusinessError) => {
            hilog.error(DOMAIN_NUMBER, TAG,
              `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`);
          });
        } catch (err) {
          // 捕获同步的参数错误
          let code = (err as BusinessError).code;
          let message = (err as BusinessError).message;
          console.error(`terminateSelf failed, code is ${code}, message is ${message}`);
          result.error("TERMINATE_ERROR", `terminateSelf error: ${message}`, null);
        }
      } else {
        console.error("UIContext is null, cannot terminate self.");
        result.error("CONTEXT_NULL", "UIContext is null", null); // Inform Flutter about the error
      }

    } else if (call.method == "removeBadge") {
      try {
        notificationManager.setBadgeNumber(0).then(() => {
          console.info(`Succeeded in removing badge number.`);
          result.success(null);
        }).catch((err: BusinessError) => {
          console.error(`Failed to remove badge number. Code is ${err.code}, message is ${err.message}`);
          result.error(err.code.toString(), err.message, null);
        });
      } catch (e) {
        const error = e as Error;
        console.error(`Error processing removeBadge: ${error.message}`);
        result.error("UNKNOWN_ERROR", error.message, null);
      }
    } else if (call.method == "isAppBadgeSupported") {
      // 鸿蒙平台默认支持角标
      result.success(true);
    } else {
      result.notImplemented();
    }
  }
}

四、编写 Example

1. 创建 Example 应用

在插件根目录下创建一个名为 example 的文件夹,用于存放示例应用。在 example 文件夹中,创建一个鸿蒙平台的 Flutter 应用,用于验证插件功能。

2. 签名与运行

使用 Deveco Studio 打开 example > ohos 目录,单击 File > Project Structure > Project > Signing Configs,勾选 Automatically generate signature,等待自动签名完成。然后运行以下命令:

flutter pub get

 flutter build hap --debug

如果应用正常启动,说明插件适配成功。如果没有,欢迎大家联系坚果派一起支持。

五、总结

通过以上步骤,我们成功地将 flutter_app_icon_badge 三方库适配到了鸿蒙平台。这个过程涉及到了解插件的基本信息、配置开发环境、创建鸿蒙模块、编写原生代码以及测试验证等多个环节。希望这篇博客能够帮助到需要进行 flutter_app_icon_badge 鸿蒙适配的开发者们,让大家在鸿蒙生态的开发中更加得心应手。

六、参考

七、坚果派

坚果派由坚果等人创建,团队拥有若干华为HDE,以及若干其他领域的三十余位万粉博主运营。专注于分享的技术包括HarmonyOS/OpenHarmony,ArkUI-X,元服务,服务卡片,华为自研语言,BlueOS操作系统、团队成员聚集在北京、上海、广州、深圳、南京、杭州、苏州、宁夏等地。 聚焦“鸿蒙原生应用”、“智能物联”和“AI赋能”、“人工智能”四大业务领域,依托华为开发者专家等强大的技术团队,以及涵盖需求、开发、测试、运维于一体的综合服务体系,赋能文旅、媒体、社交、家居、消费电子等行业客户,满足社区客户数字化升级转型的需求,帮助客户实现价值提升。 目前上架鸿蒙原生应用18款,三方库72个。

地址:https://atomgit.com/nutpi

https://gitcode.com/nutpi

八、FAQ

在刚开始适配完成后,并没有成功在桌面上生成角标,原因是没有申请权限。

收藏00

登录 后评论。没有帐号? 注册 一个。