Flutter 开发一个plugin

前言:
在Flutter实际的开发工作中,可能会遇到某些工具未提供flutter版本的情况,只有 iOS 和 Android 的SDK和集成方案,那么这种情况就需要自己开发一个 plugin 了,本文以iOS为例,写一个开发一个 plugin 的过程。
主要功能:在iOS端实现 授权 并且访问 相机,相册 在flutter展示的功能。

一. 创建 plugin 项目

我是直接用 Android Studio 创建的,也可以使用命令进行创建这个随意,勾选原生使用语言和支持平台。完成项目的创建即可。

二. 原生代码开发

iOS 项目创建完后会默认生成 PluginImagePlugin 文件
1. 创建通信通道
OC 复制代码
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {

    FlutterMethodChannel* channel = [FlutterMethodChannel

      methodChannelWithName:@"plugin_image"

            binaryMessenger:[registrar messenger]];

    PluginImagePlugin* instance = [[PluginImagePlugin alloc] init];

    [registrar addMethodCallDelegate:instance channel:channel];
    FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"plugin_camera_event" binaryMessenger:[registrar messenger]];

    [eventChannel setStreamHandler:[PluginImagePlugin sharedInstance]];

}

注册两个通道 FlutterMethodChannel 和 FlutterEventChannel 一个双向传值,一个单向传值,用两种写法来实现访问相册和相机。

2. 实现 handleMethodCall 方法

这个方法是 Flutter 和 iOS 原生代码之间的桥梁

  1. 监听:Flutter 引擎会监听 Flutter 端(Dart 代码)发起的特定方法调用。
  2. 路由 :当调用发生时,引擎会找到对应的插件,并调用其 handleMethodCall:result: 方法。
  3. 处理:你在这个方法内部编写原生代码(Objective-C 或 Swift)来处理具体的逻辑,比如调用 iOS 的系统 API、使用第三方原生库等。
  4. 回复 :处理完成后,你必须通过调用 result(...) 来给 Flutter 端一个答复,告诉它调用是成功还是失败,并返回相应的数据。
OC 复制代码
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"openCamera" isEqualToString:call.method]) {
      [self openCamera];
  } else if ([@"openPhotoAlbum" isEqualToString:call.method]) {
      [self openPhotoAlbum:result];
  }else {
    result(FlutterMethodNotImplemented);
  }
}
3. 实现 - (FlutterError*)onListenWithArguments:(id)arguments eventSink (FlutterEventSink)eventSink 方法。如果不使用 EventChannel 的话可以不实现。

简单来说,这个方法是 用来建立并处理一个从 iOS 原生端到 Flutter 端的连续事件流(Stream)

  • - (FlutterError*) : 这是一个 Objective-C 的实例方法。它返回一个 FlutterError* 对象。如果监听过程初始化成功,应该返回 nil;如果初始化失败,可以返回一个错误对象来告知 Flutter 端。

  • onListenWithArguments:eventSink: : 方法名。当 Flutter 端开始监听(listen)这个事件通道时,原生端的这个方法会被自动调用。

  • (id)arguments : 第一个参数。它是从 Flutter 端传递过来的任意参数(可以是 null, String, Map等),用于初始化事件流。例如,你可以传一个标识符来指定监听哪个传感器。

  • (FlutterEventSink)eventSink : 第二个参数,也是最重要的参数 。它的类型是 FlutterEventSink,这同样是一个函数块(Block) 。这个 eventSink 是你的 "传送带""发射器" ,你可以在原生端的任何地方(例如在定时器、代理方法、回调函数中)使用它来多次向 Flutter 端发送数据。

作用和工作流程

这个方法是一个回调函数 。它的生命周期与 Flutter 端的 Stream 紧密相连:

  1. Flutter 端开始监听 :当你在 Flutter 的代码中调用 eventChannel.receiveBroadcastStream().listen(...) 时,Flutter 引擎会向原生端发送一个"开始监听"的指令。

  2. 原生端初始化 :这个指令会触发原生端事件通道的 onListenWithArguments:eventSink: 方法被调用。

  3. 你做的事情

    • 在这个方法内部,你拿到 eventSink(数据发射器)和 arguments(初始化参数)。
  4. 保存 eventSink极其重要的一步 :你必须将这个传入的 eventSink 保存到一个强引用(strong)的属性中(例如 self.eventSink = eventSink;)。因为这个方法调用结束后,如果你不保存,这个 Block 就会被释放,你就无法再使用它发送数据了。

  5. 发送数据 :在之后的任何时间,当你需要向 Flutter 端推送数据时,就调用这个保存起来的 eventSink

    • self.eventSink(data); // 发送成功数据(可以是 NSString, NSNumber, NSDictionary 等)
    • self.eventSink([FlutterError errorWithCode:...]); // 发送一个错误
    • self.eventSink(FlutterEndOfEventStream); // 发送一个结束信号,告诉 Flutter 流已关闭
  6. 返回错误 :如果在这个初始化过程中发生了错误(比如请求的传感器不存在),你可以创建一个 FlutterError* 对象并返回,Flutter 端就会收到这个错误。如果成功,返回 nil

OC 复制代码
- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
    // 实现监听逻辑
    self.eventSink = eventSink;
    return nil;
}
4. 实现 - (FlutterError*)onCancelWithArguments:(id)arguments 方法。如果不使用 EventChannel 的话可以不实现。
作用和工作流程

这个方法也是一个回调函数,它的调用标志着事件流生命周期的结束:

  1. Flutter 端停止监听 :当你在 Flutter 的代码中调用 streamSubscription.cancel() 时,或者当 Flutter 页面销毁(Widget 被从树中移除)时,Flutter 引擎会向原生端发送一个"停止监听"的指令。

  2. 原生端清理 :这个指令会触发原生端事件通道的 onCancelWithArguments: 方法被调用。

  3. 你做的事情(核心任务)

    • 释放 eventSink至关重要的一步 :将之前在 onListen 方法中保存的 eventSink 属性设置为 nil。这打破了强引用循环(如果存在),并确保 eventSink 块被正确释放。如果不这样做,可能会导致内存泄漏。

      objc

      ini 复制代码
      self.eventSink = nil; // 必须做!
    • 释放其他为这个事件流创建的资源。

  4. 返回错误 :理论上,如果在取消过程中发生错误,你可以返回一个 FlutterError。但在绝大多数情况下,清理工作都会成功完成,直接返回 nil 即可。

OC 复制代码
- (FlutterError*)onCancelWithArguments:(id)arguments {
    // 实现取消监听逻辑
    self.eventSink = nil;
    return nil;
}
5. 业务代码实现

(1) 检测是否授权,申请权限

(2) 访问相册,访问相机

三. dart代码开发

项目创建完后会默认生成 PluginImage 和 MethodChannelPluginImage 和 PluginImagePlatform 三个类,并生成了一个默认方法 getPlatformVersion。下面简单说一下每个类的作用。

(1)PluginImagePlatform

  • 它是 一个抽象类 (Abstract Class)

  • 核心作用

    1. 定义接口 (Interface Contract): 它声明了插件对外暴露的所有公共方法(如 getPlatformVersion)。这些方法都是抽象的,只定义返回值、方法名和参数,不包含任何实现逻辑
    2. 确保单一实例 (Singleton Enforcement): 它通过 instance 的 getter 和 setter 来管理当前使用的平台实例。
    3. 安全验证 (Token Verification): 它的构造函数需要一个 Object token。这是一个安全措施,确保只有插件包内部的、拥有正确 token 的代码(如 MethodChannelPluginImage)才能继承这个类。这防止了外部代码实现不一致的接口,保证了所有实现的可靠性和一致性。

总结:只定义支持的方法,不负责执行。

代码如下:

dart 复制代码
import 'dart:async';

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'plugin_image_method_channel.dart';

abstract class PluginImagePlatform extends PlatformInterface {
  /// Constructs a PluginImagePlatform.
  PluginImagePlatform() : super(token: _token);

  static final Object _token = Object();

  static PluginImagePlatform _instance = MethodChannelPluginImage();

  late Stream deviceStream;
  late StreamController<dynamic> deviceController;

  /// The default instance of [PluginImagePlatform] to use.
  ///
  /// Defaults to [MethodChannelPluginImage].
  static PluginImagePlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [PluginImagePlatform] when
  /// they register themselves.
  static set instance(PluginImagePlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future openCamera() {
    throw UnimplementedError('openPhotoAlbum() has not been implemented.');
  }

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

定义 打开相机,相册的方法 定义 deviceStream 对象 和 StreamController 对象用于广播

(2)MethodChannelPluginImage

  • 一个具体类 (Concrete Class) ,继承自 PluginImagePlatform

  • 核心作用

    1. 实现通信逻辑: 它包含了通过 MethodChannel 与原生平台(Android/iOS)进行通信的具体代码。它在抽象父类中定义的每一个方法,在这里都会用 methodChannel.invokeMethod 来调用原生端对应的方法。
    2. 作为默认实现: 在插件注册时,它会被设置为 PluginImagePlatform.instance 的默认值。

总结:PluginImagePlatform 类的具体实现,负责与Native端进行通信

代码示例:

dart 复制代码
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'plugin_image_platform_interface.dart';

class MethodChannelPluginImage extends PluginImagePlatform {
  @visibleForTesting
  final methodChannel = const MethodChannel('plugin_image');
  final EventChannel _eventChannel = const EventChannel("plugin_camera_event");

  MethodChannelPluginImage() {
    deviceController = StreamController();
    deviceStream = deviceController.stream.asBroadcastStream();
    _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }

  void _onEvent(event) {
    Map map = event;
    if (kDebugMode) {
      debugPrint("--------------map:$map");
    }
    //添加监听
    deviceController.sink.add(map);
  }

  void _onError(Object error) {
    if (kDebugMode) {
      debugPrint("--------------error:$error");
    }
  }

  @override
  Future<String?> getPlatformVersion() async {
    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }

  @override
  Future openCamera() async {
    final version = await methodChannel.invokeMethod<String>('openCamera');
    return version;
  }

  @override
  Future<Map?> openPhotoAlbum() async {
    final imageMap = await methodChannel.invokeMethod<Map>('openPhotoAlbum');
    return imageMap;
  }
}

这行代码 deviceStream = deviceController.stream.asBroadcastStream()

将一个单订阅流(Single-Subscription Stream)转换成了一个广播流(Broadcast Stream),并将这个新的广播流赋值给 deviceStream 变量,允许同时有多个监听者。

这行代码 _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError)

建立一个监听通道 ,监听原生端发起的事件,并在_onEvent进行处理,

在**_onEvent 中 调用 deviceController.sink.add(map) 方法**,在

每一处监听的地方 deviceController.stream.listen((data) {}) 都能拿到 Native 传递的 map数据

(3)PluginImage** **

  • 数据 一个具体、唯一的公开类, 插件的入口。

  • 核心作用

    1. 提供简单API: 它拥有与 PluginImagePlatform 相同的公共方法(如 getPlatformVersion())。但它本身不实现任何逻辑 ,只是将所有调用委托(Delegate)PluginImagePlatform.instance

    2. 隐藏实现细节: 它完全隐藏了底层的平台接口、方法通道等复杂概念,为使用者提供了一个极其简洁和友好的 API。

    3. 初始化插件: 它的静态方法 registerWith() 会在插件注册时被调用,其核心任务就是将 MethodChannelPluginImage 注册为 PluginImagePlatform 的默认实现。

总结:供业务层调用的API

代码示例:

dart 复制代码
import 'dart:async';

import 'plugin_image_platform_interface.dart';

class PluginImage {
  Future<String?> getPlatformVersion() {
    return PluginImagePlatform.instance.getPlatformVersion();
  }

  /// 访问相机
  Future openCamera() {
    return PluginImagePlatform.instance.openCamera();
  }

  /// 访问相册
  Future<Map?> openPhotoAlbum() {
    return PluginImagePlatform.instance.openPhotoAlbum();
  }

  /// 相机监听
  static StreamSubscription addListen(
    void Function(dynamic event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
    return PluginImagePlatform.instance.deviceStream.listen(
      onData,
      onError: onError,
      onDone: onDone,
      cancelOnError: cancelOnError,
    );
  }
}

四. demo代码开发

具体怎么使用,就不贴代码了,请自行下载 Demo 查看。

五. flutter 和 Native交互的原理

本质就是:在 Dart 虚拟机(VM)和原生代码(Java/Kotlin, Objective-C/Swift)之间建立了一座"桥梁",通过异步消息传递来实现通信。

架构的层次关系如下图所示:

一次完整的 Dart 与 Native 的交互,其核心是一个清晰的请求-响应循环,如下图所示:

总结:

  1. 消息编码与传递 (Engine 层)

    • Dart 的调用请求(方法名 'getPlatformVersion' 和参数)会被 编码 (序列化)成一个标准的二进制格式。(在消息过桥之前,它们会被编码(Encode) 成一种紧凑的二进制格式。到达对端后,再被解码(Decode) 。)
    • 这个二进制消息通过一个叫做 BinaryMessenger 的底层接口,从 Dart 运行时(Dart VM)传递到 Flutter Engine(C++ 编写)。
  2. 原生端接收并处理 (Native 层)

    • Flutter Engine 通过 JNI (Android)Platform (iOS) 将消息路由到对应的原生平台。

    • JNI (Java Native Interface) 是 Java 虚拟机(JVM)提供的一个标准机制,它允许Java/Kotlin 代码与用 C/C++ 编写的本地代码相互调用 。 在 Flutter 的上下文中,Flutter Engine (C++) 是调用方,Android 的 Java/Kotlin 代码是被调用方

    • iOS 的调用过程比 Android 简单得多,因为 Objective-C 和 C++ 有天然的互操作性 引擎初始化 :Flutter Engine 是一个 C++ 库,它可以直接编译进 iOS 应用,直接通信 :Objective-C 是 C 的超集,C++ 代码可以直接调用 Objective-C 代码,只需将相关文件命名为 .mm(Objective-C++ 后缀)并在其中使用 Objective-C 语法即可。

  3. 结果返回 (Native -> Dart)

    • 原生代码执行完毕后,通过 result.success()result.error() 将结果或错误返回给 Flutter Engine。
    • Flutter Engine 将返回的数据同样编码 成二进制格式,通过 BinaryMessenger 传回 Dart 端。
  4. Dart 端接收结果

    • 最初 invokeMethod 返回的 Future 完成,携带来自原生端的结果。
    • Dart 代码继续执行,batteryLevel 变量被赋值,或者进入 catchError 处理错误。

Demo

相关推荐
遂心_7 分钟前
深入理解 React Hook:useEffect 完全指南
前端·javascript·react.js
Moonbit8 分钟前
MoonBit 正式加入 WebAssembly Component Model 官方文档 !
前端·后端·编程语言
龙在天14 分钟前
ts中的函数重载
前端
卓伊凡29 分钟前
非常经典的Android开发问题-mipmap图标目录和drawable图标目录的区别和适用场景实战举例-优雅草卓伊凡
前端
前端Hardy29 分钟前
HTML&CSS: 谁懂啊!用代码 “擦去”图片雾气
前端·javascript·css
前端Hardy32 分钟前
HTML&CSS:好精致的导航栏
前端·javascript·css
天下无贼43 分钟前
【手写组件】 Vue3 + Uniapp 手写一个高颜值日历组件(含跨月补全+今日高亮+选中状态)
前端·vue.js
我是天龙_绍44 分钟前
🔹🔹🔹 vue 通信方式 eventBus
前端
一个不爱写代码的瘦子1 小时前
迭代器和生成器
前端·javascript
拳打南山敬老院1 小时前
漫谈 MCP 构建之概念篇
前端·后端·aigc