给鸿蒙 App 增加广播收发能力 —— flutter_broadcasts 的鸿蒙使用指南

应用跑久了要感知息屏、组件之间要解耦传消息、多个页面要对同一份数据变化做出反应——这些场景用逐层回调或事件总线都能做,但要写不少样板代码。flutter_broadcasts 把这件事收敛成一套 API:构造 BroadcastReceiver 订阅事件,调用 sendBroadcast 发布消息,语义与 Android 的广播机制一致。该库已完成 OpenHarmony 适配,仓库见 atomgit.com/oh-flutter/flutter_broadcasts(TAG 0.4.0-ohos-1.0.0-beta.1)。读完本文,你能在鸿蒙 Flutter 应用里完成广播的订阅、发送、退订,并正确处理系统开关屏事件与后台冻结等边界情况。

一、最终运行效果

下面三张图来自适配仓库自带的 example 在 HUAWEI 真机上的实拍。第一张是应用启动后的整体界面:上方是发送卡片,下方两张接收卡片分别订阅自定义事件与系统事件:

在这里插入图片描述

第二张是点击 Send Broadcast 后的效果:Receiver A 立刻收到消息,data 为 {from: demo},消息自带时间戳,发送侧弹出 SnackBar 确认发布完成:

在这里插入图片描述

第三张是订阅系统公共事件的效果:按电源键息屏再亮屏后,Receiver B 收到带 reason 参数的 SCREEN_ON 事件:

在这里插入图片描述

二、flutter_broadcasts 是什么

flutter_broadcasts 是 pub.dev 上的跨平台广播插件(作者 Kevin Latusinski,MIT 协议,上游 0.4.0),在 Android 上基于 BroadcastReceiver/Intent,在 iOS 上基于 NSNotificationCenter。核心概念只有两个:

  • BroadcastReceiver:持有若干事件名 names 的订阅者,通过 start()/stop() 控制生命周期,收到的消息从 messages 流发出;
  • BroadcastMessage:一条消息,含事件名 name、可选参数 data、时间戳 timestamp

鸿蒙侧已由社区完成适配并保持接口不变:事件名对应 commonEventManager 的公共事件,data 对应公共事件参数。上面提到的仓库 TAG 即适配版本。

三、环境准备

鸿蒙 Flutter 环境的搭建直接照官方指南做:

Flutter OHOS 开发环境搭建指南

本文实测环境:

版本
Flutter(ohos 版)3.41.10-ohos-1.0.1(Dart 3.11.5)
编译 SDKcompatibleSdkVersion 5.1.0(18),runtimeOS HarmonyOS
实测设备HUAWEI ADA-AL10U 真机(OpenHarmony 6.1.1.120 / API 24)

运行示例需要一台已完成签名配置的鸿蒙真机或模拟器;未签名构建产物无法安装。

四、引入依赖

自建工程的标准方式是 AtomGit git 依赖,ref 锁定 TAG:

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

执行 flutter pub get 完成拉取。TAG 与框架版本对照:

Flutter(ohos 版)3.41.10-ohos-1.0.1
TAG0.4.0-ohos-1.0.0-beta.1
分支feat/ohos_flutter_broadcasts_0.4.0

以上组合在 stable 渠道实测通过;canary 引擎对宿主工程 compatibleSdkVersion 有额外要求,构建报 SDK 版本不匹配时优先检查这里。

五、代码接入

5.1 订阅广播消息

先构造 BroadcastReceiver 声明要订阅的事件名(至少一个),监听 messages 流,再调用 start() 生效:

import 'package:flutter_broadcasts/flutter_broadcasts.dart';

final receiver = BroadcastReceiver(
  names: ['com.example.app.data_changed'],
);

receiver.messages.listen((BroadcastMessage message) {
  debugPrint('received ${message.name}: ${message.data}');
});

await receiver.start();

names 是事件名列表,一个 receiver 可以同时订阅多个事件;start() 返回 Future<void>,成功即完成原生侧订阅。注意 start() 重复调用会抛 StateError,重新订阅前先确认 isListening 为 false 或调用过 stop()

5.2 发送广播

sendBroadcast 是顶层函数,构造 BroadcastMessage 传入事件名与参数即可:

await sendBroadcast(
  BroadcastMessage(
    name: 'com.example.app.data_changed',
    data: {'action': 'refresh', 'source': 'settings_page'},
  ),
);

dataMap<String, dynamic>,支持字符串、数字、布尔值与它们的嵌套组合,无法编码的类型会被降级为字符串传输。返回的 Future 在原生侧发布完成后完成,与 Android 端不同,不需要担心 Future 永不完成的问题。

5.3 停止订阅

stop() 退订原生侧并释放流订阅:

await receiver.stop();

stop() 对未启动的 receiver 是安全的空操作。配合 isListening 可以写出状态可靠的切换按钮——注意 stop() 后要 setState 刷新界面,见 5.6 的完整示例。

5.4 订阅系统公共事件

把 OpenHarmony 的系统公共事件名放进 names 即可,用法与自定义事件完全一致。以感知开关屏为例:

final systemReceiver = BroadcastReceiver(
  names: ['usual.event.SCREEN_ON', 'usual.event.SCREEN_OFF'],
);

systemReceiver.messages.listen((BroadcastMessage message) {
  if (message.name == 'usual.event.SCREEN_OFF') {
    debugPrint('screen off');
  } else if (message.name == 'usual.event.SCREEN_ON') {
    debugPrint('screen on, reason: ${message.data?['reason']}');
  }
});

await systemReceiver.start();

系统事件自带的参数会出现在 data 里,如开关屏事件的 reason 字段(POWER_KEYHARD_KEYTIMEOUT 等)。注意锁屏后应用进程被系统冻结期间事件会延迟补发,见 5.6 与第八章。

5.5 跨平台一套代码

以上代码没有任何平台判断分支:同一份 Dart 代码在 Android、iOS、鸿蒙上行为一致。这是适配"只做加法"的直接收益——业务里不需要 Platform.isOhos 之类的判断,未来上游升级时合并成本也最低。唯一需要留意的差异在系统事件名上:usual.event.* 是鸿蒙的事件命名体系,Android 对应 Intent.ACTION_*,做跨平台系统事件感知时这一层映射需要业务自己维护。

5.6 实战场景:感知息屏自动暂停后台任务

一个贴近业务的完整例子:页面里有轮询任务,息屏时暂停、亮屏时恢复,同时界面上展示订阅状态。完整片段可整体搬运:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_broadcasts/flutter_broadcasts.dart';

class ScreenAwarePollingPage extends StatefulWidget {
  const ScreenAwarePollingPage({super.key});

  
  State<ScreenAwarePollingPage> createState() => _ScreenAwarePollingPageState();
}

class _ScreenAwarePollingPageState extends State<ScreenAwarePollingPage> {
  static const _screenOn = 'usual.event.SCREEN_ON';
  static const _screenOff = 'usual.event.SCREEN_OFF';

  final BroadcastReceiver _screenReceiver = BroadcastReceiver(
    names: [_screenOn, _screenOff],
  );

  Timer? _timer;
  int _tick = 0;

  
  void initState() {
    super.initState();
    _screenReceiver.messages.listen((BroadcastMessage message) {
      if (!mounted) {
        return;
      }
      if (message.name == _screenOn) {
        _startPolling();
      } else if (message.name == _screenOff) {
        _stopPolling();
      }
      setState(() {}); // 刷新界面上的订阅状态
    });
    _screenReceiver.start();
    _startPolling();
  }

  void _startPolling() {
    _timer ??= Timer.periodic(const Duration(seconds: 2), (_) {
      setState(() => _tick++);
    });
  }

  void _stopPolling() {
    _timer?.cancel();
    _timer = null;
  }

  
  void dispose() {
    _stopPolling();
    _screenReceiver.stop();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen Aware Polling')),
      body: Center(
        child: Text(
          'listening: ${_screenReceiver.isListening}\n'
          'polling: ${_timer != null}\n'
          'ticks: $_tick',
          textAlign: TextAlign.center,
        ),
      ),
    );
  }
}

三个要点:消息回调里先判 mounted 再动 UI;disposestop() 释放订阅;isListening 是普通 getter,状态变化后必须 setState 才会反映到界面。

六、运行与验证

构建、安装、启动 example 的完整命令:

cd example
flutter build hap --debug

cd ohos
hdc install -r ../build/ohos/hap/entry-default-signed.hap
hdc shell aa start -b de.kevlatus.flutter_broadcasts_example -a EntryAbility

各核心接口的验证步骤:

发送与接收:保持 Receiver A 订阅(isListening: true),点击 Send Broadcast,消息列表出现 {from: demo} 且带时间戳,发送侧弹出完成 SnackBar(效果见第一章第二张图)。

停止订阅:点击 Receiver A 的 stop,状态变为 false;再点 Send,消息列表不新增。重新 start 后发送恢复正常(效果见适配教程图七至图九)。

系统事件:Receiver B 保持订阅,按电源键息屏再亮屏,收到带 reasonSCREEN_ON/SCREEN_OFF(效果见第一章第三张图)。

受限场景如实说明:锁屏约十秒后应用进程被系统冻结,此后的系统事件不会实时送达,解锁回前台后系统集中补发。实测中锁屏 35 分钟积累的事件在解锁后全部按序到达。这是 OpenHarmony 的进程管理行为;前台场景的自定义事件不受影响。

七、工作原理

一次广播的完整链路:

Dart: sendBroadcast(BroadcastMessage)
  │  MethodChannel 'de.kevlatus.flutter_broadcasts' / sendBroadcast
  ▼
ArkTS 插件: FlutterBroadcastsPlugin.onSendBroadcast
  │  data 展开为 Record,避免跨进程序列化丢参
  ▼
commonEventManager.publish(name, {parameters})
  │  公共事件服务跨进程分发
  ▼
订阅方 ArkTS 插件: subscribe 回调收到 CommonEventData
  │  过滤系统注入的 moduleName,补毫秒 timestamp
  │  invokeMethod('receiveBroadcast', message)
  ▼
Dart: _BroadcastChannel 单例收到回调
  │  解码为 BroadcastMessage,按 receiverId 过滤
  ▼
BroadcastReceiver.messages 流 → 业务回调

订阅方向对称:start()startReceiver,ArkTS 侧 createSubscriber + subscribe 建立监听;stop()stopReceiver,对应 unsubscribe

三个对使用方有意义的实现细节:

  • 同名双通道与平台路由:Dart 侧按 Platform.operatingSystem == 'ohos' 把调用路由到鸿蒙实现,业务层完全无感,原理细节见适配教程 2.4。
  • data 的类型边界:插件在原生侧把参数规整为编解码器可表示的类型,无法表示的降级为字符串,与 Android 端行为一致,业务不要往 data 里塞自定义对象。
  • timestamp 的语义:接收消息的 timestamp 是事件到达原生侧的时刻(毫秒),由鸿蒙端插件补齐;它与发送时刻可能有微小差值,对时间敏感的业务要注意。

八、常见问题

Q1:start() 抛 StateError 提示 already started。

同一个 BroadcastReceiver 实例不允许重复 start()。先 stop()start(),或者用 isListening 做守卫;UI 上建议把按钮状态与 isListening 绑定并在操作后 setState,从交互上杜绝重复启动。

Q2:多个 receiver 订阅同一个事件名,会都收到吗?

会。公共事件是广播语义,每个 receiver 在原生侧有独立订阅,消息按 receiverId 分发给各自的流,互不影响。

Q3:发送方自己也订阅了这个事件,能收到吗?

能。公共事件服务不区分发布方与订阅方,只要订阅了就会收到,包括本应用发出的事件。做"发送方排除自己"这类逻辑时,在 data 里带上来源标识由业务自行过滤。

Q4:锁屏后收不到系统事件,解锁后一次性收到一批。

进程冻结导致的延迟补发,不是插件丢消息。需要严格实时性的业务(如息屏立即暂停任务)请按 5.6 的模式处理——收到补发事件时状态最终一致;也可以在页面退后台时主动 stop() 订阅、回前台 start(),把补发窗口消除掉。机理详见适配教程 4.2。

Q5:data 里塞了一个自定义类实例,收到的却是字符串。

编解码器无法表示的类型会被降级为字符串传输,这是刻意的对齐行为。请在发送前把对象转成 Map<String, dynamic> 或 JSON 字符串,接收侧再解析。

九、结语与相关链接

flutter_broadcasts 用两个类覆盖了广播通信的全部常规需求,鸿蒙适配保持接口不变,业务接入成本接近于零。使用问题欢迎在鸿蒙仓库提 Issue,原库行为相关的讨论建议到上游仓库 Issue反馈。

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:

Logo

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

更多推荐