前言:
在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 原生代码之间的桥梁。
- 监听:Flutter 引擎会监听 Flutter 端(Dart 代码)发起的特定方法调用。
- 路由 :当调用发生时,引擎会找到对应的插件,并调用其
handleMethodCall:result:
方法。 - 处理:你在这个方法内部编写原生代码(Objective-C 或 Swift)来处理具体的逻辑,比如调用 iOS 的系统 API、使用第三方原生库等。
- 回复 :处理完成后,你必须通过调用
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
紧密相连:
-
Flutter 端开始监听 :当你在 Flutter 的代码中调用
eventChannel.receiveBroadcastStream().listen(...)
时,Flutter 引擎会向原生端发送一个"开始监听"的指令。 -
原生端初始化 :这个指令会触发原生端事件通道的
onListenWithArguments:eventSink:
方法被调用。 -
你做的事情:
- 在这个方法内部,你拿到
eventSink
(数据发射器)和arguments
(初始化参数)。
- 在这个方法内部,你拿到
-
保存 eventSink :极其重要的一步 :你必须将这个传入的
eventSink
保存到一个强引用(strong)的属性中(例如self.eventSink = eventSink;
)。因为这个方法调用结束后,如果你不保存,这个 Block 就会被释放,你就无法再使用它发送数据了。 -
发送数据 :在之后的任何时间,当你需要向 Flutter 端推送数据时,就调用这个保存起来的
eventSink
。self.eventSink(data);
// 发送成功数据(可以是 NSString, NSNumber, NSDictionary 等)self.eventSink([FlutterError errorWithCode:...]);
// 发送一个错误self.eventSink(FlutterEndOfEventStream);
// 发送一个结束信号,告诉 Flutter 流已关闭
-
返回错误 :如果在这个初始化过程中发生了错误(比如请求的传感器不存在),你可以创建一个
FlutterError*
对象并返回,Flutter 端就会收到这个错误。如果成功,返回nil
。
OC
- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
// 实现监听逻辑
self.eventSink = eventSink;
return nil;
}
4. 实现 - (FlutterError*)onCancelWithArguments:(id)arguments 方法。如果不使用 EventChannel 的话可以不实现。
作用和工作流程
这个方法也是一个回调函数,它的调用标志着事件流生命周期的结束:
-
Flutter 端停止监听 :当你在 Flutter 的代码中调用
streamSubscription.cancel()
时,或者当 Flutter 页面销毁(Widget 被从树中移除)时,Flutter 引擎会向原生端发送一个"停止监听"的指令。 -
原生端清理 :这个指令会触发原生端事件通道的
onCancelWithArguments:
方法被调用。 -
你做的事情(核心任务) :
-
释放 eventSink :至关重要的一步 :将之前在
onListen
方法中保存的eventSink
属性设置为nil
。这打破了强引用循环(如果存在),并确保eventSink
块被正确释放。如果不这样做,可能会导致内存泄漏。objc
iniself.eventSink = nil; // 必须做!
-
释放其他为这个事件流创建的资源。
-
-
返回错误 :理论上,如果在取消过程中发生错误,你可以返回一个
FlutterError
。但在绝大多数情况下,清理工作都会成功完成,直接返回nil
即可。
OC
- (FlutterError*)onCancelWithArguments:(id)arguments {
// 实现取消监听逻辑
self.eventSink = nil;
return nil;
}
5. 业务代码实现
(1) 检测是否授权,申请权限
(2) 访问相册,访问相机
三. dart代码开发
项目创建完后会默认生成 PluginImage 和 MethodChannelPluginImage 和 PluginImagePlatform
三个类,并生成了一个默认方法 getPlatformVersion
。下面简单说一下每个类的作用。
(1)PluginImagePlatform
-
它是 一个抽象类 (Abstract Class)
-
核心作用
- 定义接口 (Interface Contract): 它声明了插件对外暴露的所有公共方法(如
getPlatformVersion
)。这些方法都是抽象的,只定义返回值、方法名和参数,不包含任何实现逻辑。 - 确保单一实例 (Singleton Enforcement): 它通过
instance
的 getter 和 setter 来管理当前使用的平台实例。 - 安全验证 (Token Verification): 它的构造函数需要一个
Object token
。这是一个安全措施,确保只有插件包内部的、拥有正确 token 的代码(如MethodChannelPluginImage
)才能继承这个类。这防止了外部代码实现不一致的接口,保证了所有实现的可靠性和一致性。
- 定义接口 (Interface Contract): 它声明了插件对外暴露的所有公共方法(如
总结:只定义支持的方法,不负责执行。
代码如下:
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
。 -
核心作用
- 实现通信逻辑: 它包含了通过
MethodChannel
与原生平台(Android/iOS)进行通信的具体代码。它在抽象父类中定义的每一个方法,在这里都会用methodChannel.invokeMethod
来调用原生端对应的方法。 - 作为默认实现: 在插件注册时,它会被设置为
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** **
-
数据 一个具体、唯一的公开类, 插件的入口。
-
核心作用
-
提供简单API: 它拥有与
PluginImagePlatform
相同的公共方法(如getPlatformVersion()
)。但它本身不实现任何逻辑 ,只是将所有调用委托(Delegate) 给PluginImagePlatform.instance
。 -
隐藏实现细节: 它完全隐藏了底层的平台接口、方法通道等复杂概念,为使用者提供了一个极其简洁和友好的 API。
-
初始化插件: 它的静态方法
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 的交互,其核心是一个清晰的请求-响应循环,如下图所示:
总结:
-
消息编码与传递 (Engine 层) :
- Dart 的调用请求(方法名
'getPlatformVersion'
和参数)会被 编码 (序列化)成一个标准的二进制格式。(在消息过桥之前,它们会被编码(Encode) 成一种紧凑的二进制格式。到达对端后,再被解码(Decode) 。) - 这个二进制消息通过一个叫做
BinaryMessenger
的底层接口,从 Dart 运行时(Dart VM)传递到 Flutter Engine(C++ 编写)。
- Dart 的调用请求(方法名
-
原生端接收并处理 (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 语法即可。
-
-
结果返回 (Native -> Dart) :
- 原生代码执行完毕后,通过
result.success()
或result.error()
将结果或错误返回给 Flutter Engine。 - Flutter Engine 将返回的数据同样编码 成二进制格式,通过
BinaryMessenger
传回 Dart 端。
- 原生代码执行完毕后,通过
-
Dart 端接收结果:
- 最初
invokeMethod
返回的Future
完成,携带来自原生端的结果。 - Dart 代码继续执行,
batteryLevel
变量被赋值,或者进入catchError
处理错误。
- 最初