基于大模型的个人消费分析和理财助手:开发日志 5

原生层事件流系统:Pigeon EventChannel 的跨层通信设计

背景与问题

APP 的无障碍屏幕识别服务(SelectToSpeakService)在后台独立运行,而 Flutter 端的 UI(如设置页面)需要实时了解该服务的运行状态。最初的实现是页面打开时调用一次检查接口,但这意味着:

  1. 用户在其他地方关闭了无障碍服务,设置页面的开关状态仍显示为"开启"
  2. 无法在服务异常停止时及时通知用户

需要一个持续、双向的通信渠道——不是"问一下才知道",而是"变了就主动告知"。

设计思路:Pigeon + sealed class + EventChannel

项目使用 Pigeon 作为 Flutter 和 Android 原生层的类型安全通信框架。通常 Pigeon 用于定义 RPC 风格的 HostApi(Flutter 调用原生)和 FlutterApi(原生调用 Flutter),但这些都是一次性调用的模式。

这里的设计突破了常规用法:用 @EventChannelApi() 注解创建了一个持续输出的事件流接口

Pigeon 接口定义:

// front_end/pigeon.dart
sealed class PlatformEvent {}

class ScreenRecogStatusChangeEvent extends PlatformEvent {
  ScreenRecogStatusChangeEvent(this.data);
  final bool data;  // 屏幕识别服务是否开启
}

()
abstract class EventListener {
  PlatformEvent onStreamEvent();
}

核心设计点:

  1. sealed class 类型体系PlatformEvent 作为 sealed 基类,未来可以轻松扩展新的事件类型(如支付检测完成、电池状态变化等),只需要新增子类,无需修改接口定义。Dart 的 sealed 关键字确保 switch 穷举检查,不会漏掉任何事件类型。

  2. @EventChannelApi() 魔法:Pigeon 将 EventChannel 风格的 API 编译成 Kotlin 的 EventChannel 和 Dart 的 onStreamEvent() Stream 函数——比常规的 MethodChannel 调用更简洁,且天然支持流式数据。

Android 原生层:EventBus 单例

// EventBus.kt
package com.example.consumption_analyst.utils

import com.example.consumption_analyst.OnStreamEventStreamHandlerImpl
import com.example.consumption_analyst.generated.PlatformEvent

object EventBus {
    val notifier = OnStreamEventStreamHandlerImpl()
}

EventBus 是一个 Kotlin object(单例),持有 Pigeon 自动生成的 OnStreamEventStreamHandlerImpl 实例。这个 Handler 内部维护了一个 StreamController,任何地方只要拿到 EventBus.notifier,调用 onScreenRecogStatusChanged(true) 就能发送事件。

接入到生命周期:

// SelectToSpeakService.kt
override fun onCreate() {
    super.onCreate()
    startForegroundNotification()
    EventBus.notifier.onScreenRecogStatusChanged(true)  // 服务启动 → 通知
}

override fun onDestroy() {
    instance = null
    onStopCallback?.invoke()
    setOnStopCallback(null)
    EventBus.notifier.onScreenRecogStatusChanged(false)  // 服务停止 → 通知
    Log.d(TAG, "服务已停止")
}

onCreateonDestroy 是服务的天然生命周期钩子,在这里插入通知逻辑意味着:

  • 服务被系统杀死(如低内存回收)→ onDestroy 调用 → Flutter 自动收到 false
  • 用户在系统设置中关闭无障碍 → 系统触发 onDestroy → 同样自动通知

无需任何手动轮询或心跳检测。

Flutter 端:StreamSubscription 管理

// main.dart —— 全局单例
final nativeEventStream = onStreamEvent();

// settings_view.dart —— 监听并更新 UI

void initState() {
    super.initState();
    _sub = nativeEventStream.listen((event) {
        switch (event) {
            case ScreenRecogStatusChangeEvent():
                if (mounted) {
                    appSettingsController.isEnabledScreenRecog.value = event.data;
                }
        }
    });
}


void dispose() {
    _sub?.cancel();  // 页面销毁时取消订阅
    super.dispose();
}

设计要点:

  1. switch (event) + case ...: 模式匹配——利用 Dart 3 的模式匹配特性,后续新增事件类型时,编译器会提示未处理的 case,防止漏掉。

  2. mounted 检查——异步回调中页面可能已经销毁,更新 State 前必须检查 mounted,避免对已销毁的 widget 调用 setState

  3. dispose() 中调用 _sub?.cancel()——防止页面销毁后仍然收到事件回调。如果不 cancel,匿名回调持有页面引用,会导致内存泄漏。

总结

组件 角色 关键设计
pigeon.dart 跨层接口约定 sealed class + EventChannelApi
EventBus.kt Android 事件发射器 object 单例,全局可访问
SelectToSpeakService 事件源 生命周期钩子中嵌入通知
生成的 *.g.dart 自动代码 Pigeon 编译时生成 Stream 包装
Flutter Widget 事件消费者 StreamSubscription + dispose 清理

这背后的核心设计意图是:把"状态查询"转变为"状态订阅"。传统的做法会让 UI 层反复轮询,不仅浪费电量,还有延迟窗口。事件流的模式让通知延迟为零,且资源开销与事件频率成正比——没有事件时,系统是静默的。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐