Flutter插件开发指南02: 事件订阅 EventChannel

news2025/2/28 17:01:07

Flutter插件开发指南02: 事件订阅 EventChannel

视频

https://www.bilibili.com/video/BV1zj411d7k4/

前言

上一节我们讲了 Channel 通道,但是如果你是卫星定位业务,原生端主动推消息给 Flutter 这时候就要用到 EventChannel 通道了。

本节会写一个 1~50 的计数器,到 50 后自动关闭原生的消息订阅。

FlutterEventChannel

FlutterEventChannel 的作用是在 Flutter 平台和原生平台之间建立双向通信的桥梁。通过 FlutterEventChannel,Flutter 应用程序可以向原生平台发送事件,同时也可以接收来自原生平台的事件。

FlutterEventChannel 可以用于许多场景,例如:

  1. 传感器数据采集:许多应用程序需要从设备的传感器(如加速度计、陀螺仪、磁力计等)中获取数据。Flutter 应用程序可以通过 FlutterEventChannel 发送请求,让原生平台采集传感器数据并返回到 Flutter 应用程序中。
  2. 后台任务完成通知:当应用程序在后台运行时,可能需要执行一些长时间运行的任务。Flutter 应用程序可以通过 FlutterEventChannel 向原生平台发送请求,让原生平台在后台任务完成时发送通知到 Flutter 应用程序中。
  3. 音频和视频流传输:许多应用程序需要在 Flutter 应用程序和原生平台之间传输音频和视频流。Flutter 应用程序可以通过 FlutterEventChannel 向原生平台发送请求,让原生平台传输音频和视频流并返回到 Flutter 应用程序中。这使得 Flutter 应用程序可以使用原生平台的音频和视频处理功能,以提高应用程序的性能和用户体验。

FlutterEventChannel 执行过程如下:

  1. Flutter 应用程序创建一个 FlutterEventChannel 对象,并指定一个唯一的通道名称。
  2. Flutter 应用程序调用 FlutterEventChannel 的 receiveBroadcastStream 方法,以获取一个 Stream 对象,以便监听来自原生平台的事件。
  3. 原生平台创建一个 EventChannel 对象,并指定与 Flutter 应用程序中通道名称相匹配的字符串。
  4. 原生平台调用 EventChannel 的 setStreamHandler 方法,以设置一个 StreamHandler 对象,以便接收来自 Flutter 应用程序的事件并向其发送原生事件。
  5. 当 Flutter 应用程序需要向原生平台发送事件时,它会将事件数据发送到 FlutterEventChannel 对象中。
  6. FlutterEventChannel 将事件数据传递给原生平台的 EventChannel 对象。
  7. EventChannel 对象将事件数据传递给 StreamHandler 对象中的 onListen 方法。
  8. 在 onListen 方法中,原生平台可以执行一些操作并发送事件数据到 Stream 对象中。
  9. Flutter 应用程序可以通过 Stream 对象监听来自原生平台的事件,并执行相应的操作。

需要注意的是,FlutterEventChannel 中使用的 Stream 对象是异步的,因此在监听来自原生平台的事件时需要使用异步编程的技术。另外,在使用 FlutterEventChannel 时,Flutter 应用程序和原生平台之间需要约定好通道名称和事件数据格式,以便能够正确地交互和处理数据。

原文 https://ducafecat.com/blog/flutter-plugin-event-channel

参考

https://api.flutter.dev/flutter/services/EventChannel-class.html

https://mobikul.com/event-channel-in-flutter/

步骤

Flutter 插件

接口定义

lib/flutter_plugin_add_platform_interface.dart

  Future<bool?> startCounting() {
    throw UnimplementedError('startCounting() has not been implemented.');
  }

原生调用

lib/flutter_plugin_add_method_channel.dart

  @override
  Future<bool?> startCounting() async {
    final val = await methodChannel.invokeMethod<bool>('startCounting');
    return val;
  }

插件调用类

lib/flutter_plugin_add.dart

// 类型定义 - 接收函数
typedef TypeOnRecvData = void Function(int value);
  // event channel 定义
  static const eventChannel =
      EventChannel('com.ducafecat.counter/eventChannel');

  // 订阅
  StreamSubscription? _streamSubscription;

  // 接收函数
  TypeOnRecvData? _onRecvData;
  // 开始计数
  Future<void> startCounting(TypeOnRecvData onRecvData) async {
    _onRecvData = onRecvData;
    if (_streamSubscription == null) {
      bool? isStarting =
          await FlutterPluginAddPlatform.instance.startCounting();
      if (isStarting == true) {
        _streamSubscription =
            eventChannel.receiveBroadcastStream().listen(_listenStream);
      }
    }
  }
  // 取消计数
  void cancelCounting() {
    _streamSubscription?.cancel();
    _streamSubscription = null;
    _onRecvData = null;
  }
  // 接收函数
  void _listenStream(value) {
    debugPrint("Received From Native:  $value\n");
    _onRecvData?.call(value);

    if (value == 50) {
      cancelCounting();
    }
  }
  // 释放
  void dispose() {
    cancelCounting();
  }

Flutter 例子

example/lib/main.dart

  // 计数器返回
  int counterResult = 0;
  @override
  void deactivate() {
    // 释放
    _flutterPluginAddPlugin.dispose();
    super.deactivate();
  }
  @override
  Widget build(BuildContext context) {
    ...
    
              // 计数 event
              Text('count: $counterResult'),
              ElevatedButton(
                onPressed: () {
                  _flutterPluginAddPlugin.startCounting((value) {
                    setState(() {
                      counterResult = value;
                    });
                  });
                },
                child: const Text('开始计数'),
              ),
              ElevatedButton(
                onPressed: () {
                  _flutterPluginAddPlugin.cancelCounting();
                },
                child: const Text('结束计数'),
              ),

Android 端

android/src/main/java/com/ducafecat/flutter_plugin_add/FlutterPluginAddPlugin.java

成员变量

  // 日志标签
  final String TAG_NAME = "From_Native";
  // 事件通道名称
  public static final String eventChannelName = "com.ducafecat.counter/eventChannel";
  // 事件通道
  private EventChannel.EventSink eventChannel;
  // 计数器
  private int count = 0;
  // 事件 Handler
  private Handler eventHandler;
  // 消息传递器
  private BinaryMessenger binaryMessenger;

保存 BinaryMessenger

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    binaryMessenger = flutterPluginBinding.getBinaryMessenger();

启动 onMethodCall

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    ...
      
    // start
    else if (call.method.equals("startCounting")) {
      new EventChannel(binaryMessenger, eventChannelName).setStreamHandler(
              new EventChannel.StreamHandler() {
                @Override
                public void onListen(Object args, final EventChannel.EventSink events) {
                  Log.w(TAG_NAME, "Adding listener");
                  eventChannel = events;
                  count = 0;
                  eventHandler = new Handler();
                  runnable.run();
                }

                @Override
                public void onCancel(Object args) {
                  Log.w(TAG_NAME, "Cancelling listener");
                  eventHandler.removeCallbacks(runnable);
                  eventHandler = null;
                  count = 0;
                  eventChannel = null;
                  System.out.println("StreamHandler - onCanceled: ");
                }
              }
      );
      result.success(true);
    }

定时器

  private final Runnable runnable = new Runnable() {
    @Override
    public void run() {
      int TOTAL_COUNT = 50;
      if (count >= TOTAL_COUNT) {
        eventChannel.endOfStream();
      } else {
        count++;
        Log.w(TAG_NAME, "\nParsing From Native:  " + count);
        eventChannel.success(count);
      }

      eventHandler.postDelayed(this200);
    }
  };

释放

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
    if(eventHandler != null) {
      eventChannel.endOfStream();
      eventHandler.removeCallbacks(runnable);
      eventHandler = null;
      eventChannel = null;
    }
  }

IOS 端

定义成员变量

ios/Classes/FlutterPluginAddPlugin.h

// FlutterEventSink
@property (nonatomic, strong) FlutterEventSink eventSink;

// 定时器
@property (nonatomic, strong) NSTimer *timer;

// 计数器
@property (nonatomic, assign) NSInteger counter;

注册 eventChannel

ios/Classes/FlutterPluginAddPlugin.m

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
 ...
    
  // 注册事件通道
    FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"com.ducafecat.counter/eventChannel"  binaryMessenger: [registrar messenger]];
    [eventChannel setStreamHandler:instance];
}

方法调用

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  ...

  else if ([@"startCounting" isEqualToString:call.method]) {
      result(@(YES));
  }

开始订阅

- (FlutterError*)onListenWithArguments:(id)arguments
                             eventSink:(FlutterEventSink)eventSink {
    self.eventSink = eventSink;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5
                                                  target:self
                                                selector:@selector(sendEvent)
                                                userInfo:nil
                                                 repeats:YES];
    return nil;
}
// 发送消息
- (void)sendEvent {
  if (self.eventSink) {
      self.counter++;
      self.eventSink(@(self.counter));
  }
}

取消订阅

- (FlutterError*)onCancelWithArguments:(id)arguments {
    [self.timer invalidate];
    self.timer = nil;
    self.eventSink = nil;
    self.counter = 0;
    return nil;
}

最后启动

代码

https://github.com/ducafecat/flutter_develop_tips/tree/main/flutter_plugin_add

小结

使用 EventChannel 可以让插件开发更加灵活、高效、可靠和易于使用,从而可以提高插件的质量和用户体验。

还需要特别注意以下几点:

  1. 线程安全性EventChannel 的事件通信是在异步线程上执行的,因此需要确保插件代码和原生代码的线程安全性。如果在插件代码中访问 UI 线程或其他不安全的线程,可能会导致应用程序崩溃或其他问题。
  2. 内存管理:在使用 EventChannel 时需要注意内存管理。如果不及时释放资源,可能会导致内存泄漏或其他性能问题。建议在 EventChannel 不再需要时,及时停止事件监听并释放资源。
  3. 插件生命周期管理:在编写插件时,需要考虑插件的生命周期管理,如何在插件被加载和卸载时正确地初始化和释放资源。在 Flutter 中,可以使用 FlutterPlugin 接口中的 onAttachedToEngineonDetachedFromEngine 方法来管理插件的生命周期。
  4. 数据传输格式EventChannel 传输的数据格式需要在插件和原生代码之间协商一致。建议使用标准的数据传输格式,如 JSON 或 Protocol Buffers 等。
  5. 错误处理:在使用 EventChannel 时,需要考虑错误处理。如果事件通信出现错误,需要及时处理并向 Flutter 代码报告错误信息,以便及时调试和修复问题。

感谢阅读本文

如果我有什么错?请在评论中让我知道。我很乐意改进。


© 猫哥 ducafecat.com

end

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1459738.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 二维差分 二维前缀和逆运算 差分矩阵

输入一个 n 行 m 列的整数矩阵&#xff0c;再输入 q 个操作&#xff0c;每个操作包含五个整数 x1,y1,x2,y2,c &#xff0c;其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。 每个操作都要将选中的子矩阵中的每个元素的值加上 c 。 请你将进行完所有操作后的…

mfc140u.dll文丢失导致应用程序无法正常,有哪些解决办法

mfc140u.dll是Microsoft Foundation Classes&#xff08;MFC&#xff09;的一个重要组件&#xff0c;它提供了许多用于开发Windows应用程序的功能和工具。然而&#xff0c;当系统或应用程序升级、恶意软件感染或文件损坏以及用户错误操作等情况发生时&#xff0c;mfc140u.dll文…

力扣238和169

一&#xff1a;238. 除自身以外数组的乘积 1.1题目 1.2思路 1.3代码 //左右乘表 int* productExceptSelf(int* nums, int numsSize, int* returnSize) {int* answer (int*)malloc(numsSize*sizeof(int));int i 0;int left[numsSize],right[numsSize];left[0] 1;for(i 1;…

打码半年,开源一款自定义大屏设计软件!

hi&#xff0c;大家好&#xff0c;我是Tduck马马。 最近我们开源了一款大屏软件-TReport&#xff0c;与大家分享。 TReport是一款基于Vue3技术栈的数据可视化系统&#xff0c;支持静态、动态api等数据源&#xff1b;可用于数据可视化分析、报表分析、海报设计使用。 提供自定…

☀️将大华摄像头画面接入Unity 【1】配置硬件和初始化摄像头

一、硬件准备 目前的设想是后期采用网口供电的形式把画面传出来&#xff0c;所以这边我除了大华摄像头还准备了POE供电交换机&#xff0c;为了方便索性都用大华的了&#xff0c;然后全都连接电脑主机即可。 二、软件准备 这边初始化摄像头需要用到大华的Configtool软件&#…

哈希(哈希散列数据结构)---底层原理

Day02: 1.哈希散列数据结构&#xff1a;底层实现就是&#xff1a;数组链表&#xff08;红黑树&#xff09; map的put方法和get方法。 2.数组方法和链表存取数据的区别 数组方法&#xff1a;法随机访问快 链表&#xff1a;增删改效率快。 3.哈希结合了链表和数组的特性。 …

Sora后观察:AI大模型产业落地的八个锚点

在正在进行的2024年&#xff0c;国内大模型也将更下沉和落地&#xff0c;在技术上的突破之外&#xff0c;也会出现更多的向下的产业兼容和产业实践案例&#xff0c;作为新质生产力推动产业数字化转型的航船加速前进。 作者|斗斗 编辑|皮爷 出品|产业家 “电影讲述了一名…

关于深度学习和大模型的基础认知

这年头&#xff0c;作为一个技术人&#xff0c;话头里没有“大模型”&#xff0c;和人聊天都聊不下去。为了让自己和大家能更好的参与话头&#xff0c;特撰写此文&#xff0c;提供一些对大模型的基础认知能力&#xff08;门外汉&#xff0c;浅尝辄止&#xff09;。旨在解自己的…

音乐与步伐同行:南卡、韶音和墨觉的骨传导耳机深度评测

在快节奏的现代生活中&#xff0c;音乐成为了许多人精神慰藉的方式之一。特别是对于那些热爱运动的人来说&#xff0c;音乐不仅是他们运动过程中的最佳伴侣&#xff0c;更是激发潜力&#xff0c;突破极限的源动力。但是在运动的过程中如何享受到最佳的音乐体验呢&#xff1f;这…

【wu-lazy-cloud-network】Java自动化内网穿透架构整理

项目介绍 wu-lazy-cloud-network 是一款基于&#xff08;wu-framework-parent&#xff09;孵化出的项目&#xff0c;内部使用Lazy ORM操作数据库&#xff0c;主要功能是网络穿透&#xff0c;对于没有公网IP的服务进行公网IP映射 使用环境JDK17 Spring Boot 3.0.2 版本更新 1…

【快速搞定Webpack4】介绍及基本使用(一)

webpack 是一个静态资源打包工具。 他会以一个或多个文件作为打包的入口&#xff0c;将我们整个项目所有文件编译组合成一个或多个文件输出出去。 输出的文件就是编译好的文件&#xff0c;就可以在浏览器端运行了。 我们将 webpack 输出的文件叫做 bundle 。 (将浏览器不识别的…

第十三章[管理]:13.3:pycharm的常用设置

一,pycharm配置注释模板 1,打开配置界面: pycharm->preference 英文:Editor->File and Code Templates->Python Script 中文:编辑器->文件和代码模板->Python Script 如图: 我们输入的内容: # @Project : ${PROJECT_NAME} # @File : ${NAME}.py # @Author …

notepad++的下载与使用

1.进入官网下载 https://notepad-plus-plus.org/ 点击下载即可 2.选择中文简体 3.建议安装在D盘 其余步骤按照指示就行 4.安装后这几个是必选的 设置完成后就可以写中文了 以此为例 结果为

HarmonyOS(二十)——管理应用拥有的状态之LocalStorage(页面级UI状态存储)

LocalStorage是页面级的UI状态存储&#xff0c;通过Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility实例内&#xff0c;在页面间共享状态。 本文仅介绍LocalStorage使用场景和相关的装饰器&#xff1a;LocalStorageProp和LocalS…

k8s除了可以直接运行docker镜像之外,还可以运行什么? springboot项目打包成的压缩包可以直接运行在docker容器中吗?

Kubernetes&#xff08;k8s&#xff09;主要设计用于自动部署、扩展和管理容器化应用程序。虽然它与Docker容器最为密切相关&#xff0c;Kubernetes实际上是与容器运行时技术无关的&#xff0c;这意味着它不仅仅能够管理Docker容器。Kubernetes支持多种容器运行时&#xff0c;包…

ELK入门(一)-Elasticsearch(docker版)

Elasticsearch Elasticsearch安装(docker) 下载Elasticsearch 查询镜像 [rootlocalhost elk]# docker search elasticsearch NAME DESCRIPTION STARS OFFICIAL AUTOMATED elasticsearch …

Flutter学习1 - Android开发者快速上手

1、对应关系 概念对应关系 AndroidFlutter页面Activity和FragmentWidget视图ViewWidget页面跳转IntentNavigater网络库okHttphttp数据存储SharedPreference和SQLiteshared_preferences和sqflite 布局对应关系 AndroidFlutter布局文件xmlWidget线性布局LinearLayoutRow和Col…

SAP PP学习笔记 - 豆知识06 - 如何使用分类系统查找品目

PP模块&#xff0c;之前学习PP模块主数据的时候&#xff0c;其中一个主数据叫分类系统&#xff08;Classification View&#xff09;。 它的用处是定义一些SAP没提供的特殊字段&#xff0c;以做查询&#xff0c;分析用。 有关分类系统的详细&#xff0c;可以参照如下文章。 …

Oracle 如何提高空间使用率?

一&#xff0c;行迁移和行链接。 oracle尽量保证一行的数据能够放在同一个数据块当中&#xff0c;有的时候行会发生行迁移和行链接。 行链接 &#xff1a;有一个列的字段是大对象&#xff08;long&#xff0c;longlong&#xff09;一行占的数据一整个块都放不下&#xff0c;则…

如何在Excel中冻结行或列标题?这里提供两种方法

随着数据的增长&#xff0c;许多Excel工作表可能会变得很大&#xff0c;因此冻结行和列标题或冻结窗格非常有用&#xff0c;以便在滚动工作表时将标题锁定到位。在Excel中&#xff0c;可以冻结行标题和列标题&#xff0c;也可以只冻结一个。这不会影响将要打印的单元格。列标题…