Flutter 三方库鸿蒙适配实践:以 Firebase Messaging 为例实现跨平台推送集成

Flutter 三方库鸿蒙适配实践:以 Firebase Messaging 为例实现跨平台推送集成

引言:Flutter 遇上 HarmonyOS

如今多端融合的趋势下,Flutter 凭借高效的渲染和“一次编写,多端部署”的特性,成为了很多开发团队的首选。不过,当应用需要上架华为鸿蒙(HarmonyOS)这类新兴系统时,一个实际问题就出现了:Flutter 丰富的第三方插件(比如推送、地图、支付)大多依赖 Android/iOS 的原生实现,而鸿蒙的应用框架(ArkUI)、API 设计与系统服务跟 Android 有着本质上的不同,导致很多插件无法直接运行。

推送功能就是个典型例子。firebase_messaging 是 Flutter 生态里最常用的 Firebase 云消息插件,但它的官方实现只支持 Android(FCM)和 iOS(APNs)。如果你希望一个依赖该插件的 Flutter 应用能在鸿蒙设备上正常接收推送,就不得不为它重新实现一套鸿蒙端的原生逻辑

本文就以 firebase_messaging 插件为例,详细梳理 Flutter 三方库适配鸿蒙的完整过程。我们会从底层通信机制讲起,理清适配思路,并给出从架构设计、代码实现、集成调试到性能优化的一整套可行方案,帮助开发者更顺畅地跨过平台之间的生态差异。

一、技术架构与适配原理

1.1 Flutter 插件与平台通道(Platform Channel)机制

Flutter 和原生平台之间通过 Platform Channel 进行异步消息通信。对插件开发者来说,关键是理解它的三层结构:

  1. Dart 层:在 Flutter 应用中调用插件提供的 API。
  2. 平台层:在 Android(Java/Kotlin)和 iOS(Objective-C/Swift)中实现具体的功能。
  3. 桥接层:通过 MethodChannel(方法调用)、EventChannel(事件流)等进行数据传递。

适配鸿蒙的核心,其实就是在鸿蒙侧(使用 ArkTS/Java)重新实现插件的 “平台层”,并确保它能通过 Channel 与 Flutter 的 Dart 层正确通信。好在 OpenHarmony 社区的 flutter_ohos 项目已经提供了 Flutter 引擎在鸿蒙上的基础支持,实现了 Channel 的底层通信能力,这为我们适配插件打下了基础。

1.2 鸿蒙推送生态与 Firebase 的功能对照

firebase_messaging 在 Android 端主要依赖 Google Play 服务和 FCM SDK。在鸿蒙上,我们需要找到对等的系统服务——也就是 华为 Push Kit。但要注意,这并非简单的 API 替换,两者在设计和接口上都有不少区别:

功能模块Firebase Cloud Messaging (FCM)华为 Push Kit适配策略
初始化与令牌自动初始化,通过 onTokenRefresh 获取 FCM Token需手动调用 getToken 接口申请 HMS Push Token在鸿蒙端封装 Token 获取逻辑,通过 MethodChannel 返回给 Dart。
接收消息通过继承 FirebaseMessagingService 处理后台消息通过 PushReceiverAbility 接收透传消息和通知创建鸿蒙 Ability 作为消息接收器,解析后通过 EventChannel 转发至 Flutter。
主题订阅提供 subscribeToTopic / unsubscribeFromTopic API提供 subscribe / unsubscribe 主题接口在鸿蒙端实现对应的主题管理方法,与 Push Kit SDK 交互。
前台消息处理可通过 onMessage 监听前台消息流Push Kit 通知默认展示,透传消息需在接收器内处理在接收器 Ability 中判断应用状态,区分处理并转发。

适配的核心思路:在鸿蒙端,我们构建一个叫 firebase_messaging_ohos 的插件。它作为 firebase_messaging 在鸿蒙上的“替身”,保持 Dart API 完全一致,但底层将所有对 FCM 的调用,转为对华为 Push Kit 的调用。

二、适配方案设计与关键代码实现

2.1 整体架构设计

我们为鸿蒙端设计一个双层的插件结构:

  • Flutter Plugin 接口层:实现标准 Flutter 插件接口,负责与 Dart 侧通信。
  • 鸿蒙 Push Kit 封装层:封装华为 Push Kit 的初始化、Token 管理、消息接收等原生逻辑。
Flutter Dart App (firebase_messaging API)
         ↓
MethodChannel / EventChannel
         ↓
鸿蒙端 `firebase_messaging_ohos` 插件
         ├── PushService (管理Token、主题)
         ├── PushAbility (接收并处理推送消息)
         └── PushEventDispatcher (向Flutter转发事件)
         ↓
华为 Push Kit SDK
         ↓
华为推送服务

2.2 核心代码实现

2.2.1 Dart 层(保持接口一致)

为了不改动业务代码,我们新建一个插件包(例如 firebase_messaging_ohos),其 Dart API 与官方 firebase_messaging 完全一致。

// firebase_messaging_ohos.dart
class FirebaseMessagingOhos {
  static const MethodChannel _channel = 
      MethodChannel('plugins.flutter.io/firebase_messaging_ohos');
  static const EventChannel _eventChannel = 
      EventChannel('plugins.flutter.io/firebase_messaging_ohos/events');

  // 获取设备令牌
  static Future<String?> getToken() async {
    return await _channel.invokeMethod<String?>('getToken');
  }

  // 订阅主题
  static Future<void> subscribeToTopic(String topic) async {
    await _channel.invokeMethod('subscribeToTopic', {'topic': topic});
  }

  // 监听消息流(前后台消息)
  static Stream<RemoteMessage> get onMessage {
    return _eventChannel.receiveBroadcastStream().map((dynamic event) {
      return RemoteMessage.fromMap(Map<String, dynamic>.from(event));
    });
  }

  // 初始化配置,行为与原插件对齐
  static Future<void> initialize({
    FirebaseOptions? options,
    bool? automaticDataCollectionEnabled,
  }) async {
    // 鸿蒙端初始化主要触发 Push Kit SDK 初始化
    await _channel.invokeMethod('initialize');
  }
}
2.2.2 鸿蒙端实现 - Push Service(核心服务)

这部分负责与 Push Kit SDK 直接交互。

// PushService.java
package com.example.firebasemessagingohos;

import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.rpc.RemoteException;
import ohos.push.PushAgent;
import ohos.push.PushCallback;
import ohos.push.PushManager;
import ohos.push.model.TokenInfo;

public class PushService {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "PushService");
    private final Context context;
    private PushAgent pushAgent;

    public PushService(Context context) {
        this.context = context;
        initPushKit();
    }

    private void initPushKit() {
        pushAgent = PushManager.getPushAgent(context);
        pushAgent.setPushCallback(new PushCallback() {
            @Override
            public void onTokenReceived(TokenInfo tokenInfo) {
                // Token 通常会在此回调,但我们主动获取更稳妥
                HiLog.info(LABEL, "Push Token received (callback).");
            }

            @Override
            public void onPushMsgStateChanged(boolean isSuccess, String messageId, int errorCode) {
                HiLog.debug(LABEL, "Message state changed: %{public}s, code: %{public}d", messageId, errorCode);
            }
        });
    }

    // 获取 Token - 对应 Dart 端的 getToken()
    public String getToken() throws PushAdapterException {
        try {
            // 注意:需确保已在 agconnect-services.json 中配置好应用信息
            String token = PushManager.getToken(context);
            if (token == null || token.isEmpty()) {
                throw new PushAdapterException("Failed to get push token: token is empty.");
            }
            HiLog.info(LABEL, "Successfully got Push Token: %{public}s", token);
            return token;
        } catch (RemoteException e) {
            HiLog.error(LABEL, "RemoteException when getting token: %{public}s", e.getMessage());
            throw new PushAdapterException("RemoteException: " + e.getMessage(), e);
        } catch (Exception e) {
            HiLog.error(LABEL, "Exception when getting token: %{public}s", e.getMessage());
            throw new PushAdapterException("Exception: " + e.getMessage(), e);
        }
    }

    // 订阅主题
    public void subscribeToTopic(String topic) throws PushAdapterException {
        try {
            PushManager.subscribe(context, topic);
            HiLog.info(LABEL, "Subscribed to topic: %{public}s", topic);
        } catch (RemoteException e) {
            HiLog.error(LABEL, "Failed to subscribe to topic: %{public}s", e.getMessage());
            throw new PushAdapterException("Subscribe failed: " + e.getMessage(), e);
        }
    }

    // 自定义异常,便于统一处理错误
    public static class PushAdapterException extends Exception {
        public PushAdapterException(String message) { super(message); }
        public PushAdapterException(String message, Throwable cause) { super(message, cause); }
    }
}
2.2.3 鸿蒙端实现 - Push Ability(消息接收器)

负责接收系统推送,并转发给 Flutter。

// PushAbility.java (继承自Ability,需在config.json中声明)
package com.example.firebasemessagingohos;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.push.PushManager;
import ohos.push.model.PushMsg;
import java.util.HashMap;
import java.util.Map;

public class PushAbility extends Ability {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "PushAbility");
    private static EventSink eventSink; // 用于向 Flutter 发送事件流的接口,需从 Plugin 类注入

    public static void setEventSink(EventSink sink) {
        eventSink = sink;
    }

    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);
        HiLog.info(LABEL, "PushAbility started.");
        // 处理冷启动时携带推送数据的 Intent
        processIntent(intent);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        HiLog.info(LABEL, "PushAbility onNewIntent.");
        // 处理从通知栏点击打开应用时携带的数据
        processIntent(intent);
    }

    private void processIntent(Intent intent) {
        if (intent == null) return;
        // 解析 Push Kit 传递过来的消息数据
        PushMsg pushMsg = intent.getSerializableParam(PushManager.MESSAGE_KEY);
        if (pushMsg != null) {
            Map<String, Object> messageMap = parsePushMsgToMap(pushMsg);
            // 通过 EventChannel 发送给 Flutter 端(模拟 onMessageOpenedApp)
            if (eventSink != null) {
                eventSink.success(messageMap);
            }
            HiLog.info(LABEL, "Processed push message from intent: %{public}s", pushMsg.getContent());
        }
    }

    // 静默推送或后台消息会触发此方法(需在 config.json 中配置相应权限)
    public void onPushMsg(PushMsg pushMsg) {
        HiLog.info(LABEL, "Received push message in background: %{public}s", pushMsg.getContent());
        Map<String, Object> messageMap = parsePushMsgToMap(pushMsg);
        // 转发给 Flutter 的 onMessage 流
        if (eventSink != null) {
            eventSink.success(messageMap);
        }
        // 根据消息内容决定是否创建本地通知
        if (pushMsg.getNotification() != null) {
            createLocalNotification(pushMsg);
        }
    }

    private Map<String, Object> parsePushMsgToMap(PushMsg pushMsg) {
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", pushMsg.getMessageId());
        map.put("data", pushMsg.getContent()); // 通常是 JSON 字符串
        map.put("from", pushMsg.getDeviceToken());
        map.put("sentTime", pushMsg.getSendTime());
        map.put("ttl", pushMsg.getTtl());
        // 可在此解析 content 中的自定义键值对
        // ... 解析逻辑
        return map;
    }

    private void createLocalNotification(PushMsg pushMsg) {
        // 使用鸿蒙的 NotificationHelper 创建本地通知
        // ... 实现创建通知的代码
    }
}
2.2.4 鸿蒙端实现 - Flutter 插件主类

整合服务与通信,实现 FlutterPluginMethodChannel 接口。

// FirebaseMessagingOhosPlugin.java
package com.example.firebasemessagingohos;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

public class FirebaseMessagingOhosPlugin implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "FMPPlugin");
    private MethodChannel methodChannel;
    private EventChannel eventChannel;
    private Context applicationContext;
    private PushService pushService;
    private EventChannel.EventSink eventSink;

    @Override
    public void onAttachedToEngine(FlutterPluginBinding binding) {
        applicationContext = binding.getApplicationContext();
        methodChannel = new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/firebase_messaging_ohos");
        methodChannel.setMethodCallHandler(this);

        eventChannel = new EventChannel(binding.getBinaryMessenger(), "plugins.flutter.io/firebase_messaging_ohos/events");
        eventChannel.setStreamHandler(this);

        pushService = new PushService(applicationContext);
        HiLog.info(LABEL, "FirebaseMessagingOhosPlugin attached.");
    }

    @Override
    public void onMethodCall(MethodCall call, Result result) {
        try {
            switch (call.method) {
                case "getToken":
                    String token = pushService.getToken();
                    result.success(token);
                    break;
                case "subscribeToTopic":
                    String topic = call.argument("topic");
                    pushService.subscribeToTopic(topic);
                    result.success(null);
                    break;
                case "initialize":
                    // 初始化逻辑,确保 PushAgent 已就绪
                    result.success(null);
                    break;
                default:
                    result.notImplemented();
            }
        } catch (PushService.PushAdapterException e) {
            HiLog.error(LABEL, "Method call %{public}s failed: %{public}s", call.method, e.getMessage());
            result.error("PUSH_SERVICE_ERROR", e.getMessage(), e.getCause() != null ? e.getCause().toString() : null);
        }
    }

    // EventChannel.StreamHandler 方法
    @Override
    public void onListen(Object arguments, EventChannel.EventSink events) {
        this.eventSink = events;
        PushAbility.setEventSink(events); // 将 sink 传递给 Ability,用于发送消息
        HiLog.info(LABEL, "Flutter 端开始监听消息事件流");
    }

    @Override
    public void onCancel(Object arguments) {
        this.eventSink = null;
        PushAbility.setEventSink(null);
        HiLog.info(LABEL, "Flutter 端取消监听消息事件流");
    }

    @Override
    public void onDetachedFromEngine(FlutterPluginBinding binding) {
        methodChannel.setMethodCallHandler(null);
        eventChannel.setStreamHandler(null);
        HiLog.info(LABEL, "FirebaseMessagingOhosPlugin detached.");
    }
}

三、集成步骤与调试建议

3.1 环境准备与配置

  1. 开发环境:安装 DevEco Studio,配置好 HarmonyOS SDK 和 flutter_ohos 工具链。
  2. 项目配置:在鸿蒙应用的 config.json 中声明推送权限和 Push Ability。
    {
      "app": {
        "bundleName": "com.your.app",
        "vendor": "your.vendor"
      },
      "module": {
        "abilities": [
          {
            "name": "com.example.firebasemessagingohos.PushAbility",
            "type": "service",
            "visible": true,
            "permissions": ["ohos.permission.PUSH"]
          }
        ],
        "reqPermissions": [
          {
            "name": "ohos.permission.PUSH",
            "reason": "用于接收华为推送消息"
          }
        ]
      }
    }
    
  3. 依赖引入:在鸿蒙模块的 build.gradle 中添加 Push Kit SDK 依赖。
  4. 华为侧配置:在华为开发者联盟创建应用,开通 Push Kit 服务,配置好应用签名证书指纹(.p7b),并把下载的 agconnect-services.json 文件放到鸿蒙应用模块的 entry/src/main/resources/rawfile/ 目录下。

3.2 代码集成与测试

  1. 替换插件依赖:在 Flutter 项目的 pubspec.yaml 中,将 firebase_messaging 换成我们开发的 firebase_messaging_ohos(或已发布的适配版本)。
  2. 初始化:在 Flutter 应用启动时,调用 FirebaseMessagingOhos.initialize()
  3. 获取 Token:在适当位置调用 FirebaseMessagingOhos.getToken() 并上传到你的应用服务器。
  4. 监听消息:设置 FirebaseMessagingOhos.onMessage 监听器来处理收到的消息。
  5. 测试推送
    • 使用 华为推送调试工具(HMS Toolkit)发送测试消息。
    • 在服务端集成华为推送服务端 API,针对设备 Token 发送推送。
    • 分别测试 前台后台应用关闭 三种状态下的消息接收与展示。

3.3 性能与稳定性注意点

  • 冷启动耗时:注意鸿蒙端初始化 Push Kit 和获取 Token 的时间,避免影响应用启动速度。
  • 内存占用:常驻的 Push Ability 要留意内存使用,避免不必要的对象引用。
  • 网络兼容性:确保弱网环境下 Token 获取和消息接收的稳定性,可以考虑加入重试机制。
  • 功耗:监听网络状态变化,避免频繁重连导致耗电过快。
  • 兼容性:在不同 HarmonyOS 版本(如 API 8、9)上测试插件行为是否一致。

四、总结

通过以上步骤,我们基本完成了 firebase_messaging 插件在鸿蒙上的功能适配。这个案例也展示了 Flutter 三方库鸿蒙适配的通用方法:

  1. 接口保持一致:Dart 层 API 不变,业务代码无需修改。
  2. 利用通道通信:复用 Flutter Platform Channel 进行跨平台通信。
  3. 功能映射替换:理解鸿蒙对应服务的 API,完成等价功能替换。
  4. 工程化封装:将原生代码封装清晰,提供良好的错误处理和日志。

这种适配不仅是技术实现,更是一种应对多系统生态的设计思路。对于地图、支付、生物识别等其他核心插件,其实都可以参照类似的路径进行适配。

如果能将这类适配方案沉淀为开源项目或工具,将会大幅降低 Flutter 应用向鸿蒙迁移的成本,真正发挥 Flutter 在多平台统一开发上的优势。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值