Flutter & Native 混合开发
项目集成方案
问题
- 项目中仍有大量业务使用 Native 开发。项目引入 Flutter 后,要求所有 Native 开发者都配置 Flutter 开发环境并改动项目工程结构,会对开发效率造成影响。
- 引入 Flutter 后,混合项目的构建流程会发生变动。打包环境需要配置 Flutter 开发环境,且 Native 项目无法单独构建。
需求
- 独立开发环境
- 独立构建
Flutter 项目模式
Flutter 模式就是纯 Flutter 开发模式,原生项目被包在 Flutter 项目中,通过 Flutter 命令启动项目,命令会先编译 Flutter 工程,然后将 Flutter 编译产物复制到 Native 项目中,接着编译 Native ,最后启动 Native ,Native 调用 Flutter 。
module 模式,Flutter 工程只负责产出 Flutter 编译产物,提供给 Native 工程使用。Flutter 和 Native 工程没有代码和环境依赖,Native 工程只依赖 Flutter 的编译产物。Flutter 工程和 Native 工程可以独立编译。
我们通过 flutter create
命令创建新的 Flutter 工程。这个命令有一个 --type
参数,通过传入 module
这个参数 , 命令会为我们创建一个 module 类型的 Flutter 项目。它和普通的 Flutter 项目只有些许不同,我们可以通过项目结构窥探一二。
从上面的图片可知,Flutter 项目模式和 module 模式在项目结构和文件上差不多,但在 iOS 和 Android 这两个和 Native 相关的文件夹上,Flutter 项目是直接将两个文件夹暴露出来,而 module 模式则是选择隐藏这两个 Native 文件夹。很显然,对于 Flutter 项目而言,Native 项目直接包在了 Flutter 项目中,必然需要在其中直接进行 Native 页面和逻辑的开发。而 module 模式中,整个 Flutter 工程是和 Native 工程隔离开的,原则上不应该有 Native 的逻辑代码,因而隐藏了 Native 相关文件,不让用户有机会触碰或者添加 Native 代码。
既然 module 模式和 Native 项目完全隔离, 为什么还要保留一个隐藏的 Native 的项目呢?因为在 Native 平台上,Flutter 无法独立运行, 始终需要依附于一个 Native 项目才能够启动。因此为了在 module 模式下运行 App 进行调试,必定需要一个 Native App 的空壳来调起 Flutter 页面。
Native 调用 Flutter
要理解 module 模式如何工作,我们首先需要了解,一个纯 Flutter 工程是如何被 Native App 引用并调用的。
Flutter for iOS 的产物:
- App.framework:Dart 业务源码相关文件,以及项目依赖的静态资源,如字体,图片等
- Flutter.framework:Flutter 引擎库文件
- pubs 插件目录及用于索引的文件:Flutter 下的插件,包括各种系统的和自定义的channels
iOS Native 项目通过引入 Flutter.framework ,便可以调用 Flutter 项目中的代码,将 Flutter 嵌入到项目中。
Flutter for Android 的产物则是一个 flutter.aar,其中同样包含了 Flutter 引擎库以及项目中用到的静态资源等。Android 工程通过引入 flutter.aar ,便可以调用 Flutter 项目中的代码。
从上面的实践可知,只要我们取得了 Flutter for Native 的编译产物,便可以轻松的将 Flutter 工程嵌入到现有的 Native 项目中。其中,iOS 相对简单,直接将 Flutter 编译出的几个产物(Framwork、配置文件) 拖入 Native 项目中即可调用。安卓则需要修改 Native 工程中的 gradle,添加 aar 中的 Flutter 依赖。
Native 接入 Flutter 实践
目录结构如下
some/path/ flutter_host_android/ flutter_host_ios/ flutter_module/
保持 Native 和 Flutter 工程入口在同一目录下,三个工程各自由自己的 Git 进行版本管理。
flutter_module 目录下包含了 Flutter 开发人员编写的 Flutter 代码和 Flutter 的编译产物。对于 Native 工程而言,他们不关心其中的 Flutter 代码,他们只需要将资源定位到其中的编译产物,然后引入项目即可。
Android 引入 Flutter
在工程 settings.gradle
文件中添加
//增加内容
setBinding(new Binding([gradle: this]))
evaluate(new File( settingsDir.parentFile,'flutter_module/.android/include_flutter.groovy' ))
在工程 build.gradle
文件中添加
dependencies {
.
.
implementation project(':flutter')
}
当然配置完 gradle 后不要忘记同步。
需要注意的是,Flutter 开发人员在编写完代码提交到 Git 之前,需要执行一次完整的 Flutter 项目,以便生成最新的编译产物,否则 Native 端在调用 Flutter 时不会执行最新的代码。
当然,结偶性更好的方案是,只给 Native 开发人员提供 Flutter 产物,甚至使用 Cocoapods 这类包管理工具进行自动导入和版本控制。大家可以根据自己的需求来实现具体的工程配置。
Flutter & Native 交互
Flutter 和 Native 之间的交互以及数据交换传输,依赖于一种称为 Platform Channel 的工具。
Platform Channel
Platform Channel 主要做了三件事:
- 发送消息
- 监听发送过来的消息
- 对不同平台的数据类型进行自动转换
Flutter 定义了三种不同类型的 Channel :
- BasicMessageChannel:用于传递字符串和半结构化的信息。
- MethodChannel:用于传递方法调用(method invocation)。
- EventChannel: 用于数据流(event streams)的通信。
三种 Channel 看似复杂,而且他们都有各自的使用场景,但他们在设计上却非常相近。
每种 Channel 均有三个重要成员变量:
- name: String 类型,代表 Channel 的名字,也是其唯一标识符。
- messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具。
- codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器。
Channel name
一个 Flutter 应用中可能存在多个 Channel ,每个 Channel 在创建时必须指定一个独一无二的 name ,Channel 之间使用 name 来区分彼此。当有消息从 Flutter 端发送到 Platform 端时,会根据其传递过来的 channel name 找到该 Channel 对应的 Handler(消息处理器)。
BinaryMessenger 消息信使
虽然三种 Channel 用途不同,但是他们与 Flutter 通信的工具却是相同的,均为 BinaryMessager 。
BinaryMessenger 是 Platform 端与 Flutter 端通信的工具,其通信使用的消息格式为二进制格式数据。当我们初始化一个 Channel ,并向该 Channel 注册处理消息的 Handler 时,实际上会生成一个与之对应的 BinaryMessageHandler ,并以 channel name 为 key ,注册到 BinaryMessenger 中。当Flutter端发送消息到 BinaryMessenger 时,BinaryMessenger 会根据其入参 channel 找到对应的 BinaryMessageHandler ,并交由其处理。
Binarymessenger 在 Android 端是一个接口,其具体实现为 FlutterNativeView 。而其在 iOS 端是一个协议,名称为 FlutterBinaryMessenger ,FlutterViewController 遵循了它。
Binarymessenger 并不知道 Channel 的存在,它只和 BinaryMessageHandler 打交道。而 Channel 和 BinaryMessageHandler 则是一一对应的。由于 Channel 从 BinaryMessageHandler 接收到的消息是二进制格式数据,无法直接使用,故 Channel 会将该二进制消息通过 Codec(消息编解码器)解码为能识别的消息并传递给 Handler 进行处理。
当 Handler 处理完消息之后,会通过回调函数返回 result ,并将 result 通过编解码器编码为二进制格式数据,通过 BinaryMessenger 发送回 Flutter 端。
消息编解码器:Codec
消息编解码器 Codec 主要用于将二进制格式的数据转化为 Handler 能够识别的数据,Flutter 定义了两种 Codec :MessageCodec 和 MethodCodec 。
MessageCodec
MessageCodec 用于二进制格式数据与基础数据之间的编解码。BasicMessageChannel 所使用的编解码器就是MessageCodec。
Android 中,MessageCodec 是一个接口,定义了两个方法:encodeMessage
接收一个特定的数据类型 T,并将其编码为二进制数据 ByteBuffer ,而decodeMessage
则接收二进制数据 ByteBuffer ,将其解码为特定数据类型 T。iOS 中,其名称为 FlutterMessageCodec ,是一个协议,定义了两个方法:encode
接收一个类型为 id 的消息,将其编码为 NSData 类型,而decode
接收 NSData 类型消息,将其解码为 id 类型数据。
MessageCodec 有多种不同的实现:
StandardMessageCodec
StandardMessageCodec 是 BasicMessageChannel 的默认编解码器,其支持基础数据类型、二进制数据、列表、字典。
BinaryCodec
BinaryCodec 是最为简单的一种 Codec ,因为其返回值类型和入参的类型相同,均为二进制格式( Android 中为 ByteBuffer ,iOS 中为 NSData )。实际上,BinaryCodec 在编解码过程中什么都没做,只是原封不动将二进制数据消息返回而已。或许你会因此觉得 BinaryCodec 没有意义,但是在某些情况下它非常有用,比如使用 BinaryCodec 可以使传递内存数据块时在编解码阶段免于内存拷贝。
StringCodec
StringCodec 用于字符串与二进制数据之间的编解码,其编码格式为UTF-8。
JSONMessageCodec
JSONMessageCodec 用于基础数据与二进制数据之间的编解码,其支持基础数据类型以及列表、字典。其在 iOS 端使用了 NSJSONSerialization 作为序列化的工具,而在 Android 端则使用了其自定义的 JSONUtil 与 StringCodec 作为序列化工具。
MethodCodec
MethodCodec 用于二进制数据与方法调用( MethodCall )和返回结果之间的编解码。MethodChannel 和 EventChannel 所使用的编解码器均为 MethodCodec 。
与 MessageCodec 不同的是,MethodCodec 用于 MethodCall 对象的编解码,一个 MethodCall 对象代表一次从 Flutter 端发起的方法调用。MethodCall 有2个成员变量:String 类型的method
代表需要调用的方法名称,通用类型( Android 中为 Object ,iOS 中为 id )的arguments
代表需要调用的方法入参。
由于处理的是方法调用,故相比于 MessageCodec ,MethodCodec 多了对调用结果的处理。当方法调用成功时,使用encodeSuccessEnvelope
将 result 编码为二进制数据,而当方法调用失败时,则使用encodeErrorEnvelope
将 error 的 code 、message 、detail 编码为二进制数据。
MethodCodec 有两种实现:
StandardMethodCodec
MethodCodec 的默认实现,StandardMethodCodec 的编解码依赖于 StandardMessageCodec ,当其编码 MethodCall 时,会将 method 和 args 依次使用 StandardMessageCodec 编码,写入二进制数据容器。其在编码方法的调用结果时,若调用成功,会先向二进制数据容器写入数值0(代表调用成功),再写入 StandardMessageCodec 编码后的 result 。而调用失败,则先向容器写入数据1(代表调用失败),再依次写入 StandardMessageCodec 编码后的 code ,message 和 detail 。
JSONMethodCodec
JSONMethodCodec 的编解码依赖于 JSONMessageCodec ,当其在编码 MethodCall 时,会先将 MethodCall 转化为字典
{"method":method,"args":args}
。其在编码调用结果时,会将其转化为一个数组,调用成功为[result]
,调用失败为[code,message,detail]
。再使用 JSONMessageCodec 将字典或数组转化为二进制数据。
消息处理器:Handler
当我们接收二进制格式消息并使用 Codec 将其解码为 Handler 能处理的消息后,就该 Handler 上场了。Flutter 定义了三种类型的 Handler ,与 Channel 类型一一对应。我们向 Channel 注册一个 Handler 时,实际上就是向 BinaryMessager 注册一个与之对应的 BinaryMessageHandler 。当消息派分到 BinaryMessageHandler 后,Channel 会通过 Codec 将消息解码,并传递给 Handler 处理。
MessageHandler
MessageHandler 用户处理字符串或者半结构化的消息,其onMessage
方法接收一个 T 类型的消息,并异步返回一个相同类型 result 。MessageHandler 的功能比较基础,使用场景较少,但是其配合 BinaryCodec 使用时,能够方便传递二进制数据消息。
MethodHandler
MethodHandler 用于处理方法的调用,其onMessage
方法接收一个 MethodCall 类型消息,并根据 MethodCall 的成员变量method
去调用对应的 API ,当处理完成后,根据方法调用成功或失败,返回对应的结果。
StreamHandler
StreamHandler 与前两者稍显不同,用于事件流的通信,最为常见的用途就是 Platform 端向 Flutter 端发送事件消息。当我们实现一个 StreamHandler 时,需要实现其onListen
和onCancel
方法。而在onListen
方法的入参中,有一个 EventSink(其在 Android 是一个对象,iOS 端则是一个 block )。我们持有 EventSink 后,即可通过 EventSink 向 Flutter 端发送事件消息。
实际上,StreamHandler 工作原理并不复杂。当我们注册了一个 StreamHandler 后,实际上会注册一个对应的 BinaryMessageHandler 到 BinaryMessager 。而当 Flutter 端开始监听事件时,会发送一个二进制消息到 Platform 端。Platform 端用 MethodCodec 将该消息解码为 MethodCall ,如果 MethodCall 的 method 的值为 "listen" ,则调用 StreamHandler 的onListen
方法,传递给 StreamHandler 一个 EventSink 。而通过 EventSink 向 Flutter 端发送消息时,实际上就是通过 BinaryMessager 的 send 方法将消息传递过去。
Flutter & Native 交互实操
BasicMessageChannel
Flutter
static const messageChannel = const BasicMessageChannel('samples.flutter.io/message', StandardMessageCodec());
static const messageChannel2 = const BasicMessageChannel('samples.flutter.io/message2', StandardMessageCodec());
Future<String> sendMessage() async {
String reply = await messageChannel.send('发送给Native端的数据');
print('reply: $reply'); return reply;
}
void receiveMessage() {
messageChannel2.setMessageHandler((message) async {
print('message: $message');
return '返回Native端的数据';
});
}
@override void initState() {
// TODO:
implement initState super.initState();
receiveMessage();
sendMessage();
}
iOS
// 初始化定义
FlutterBasicMessageChannel* messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"samples.flutter.io/message" binaryMessenger:controller];
// 接收消息监听
[messageChannel setMessageHandler:^(id message, FlutterReply callback) {
NSLog(message);
callback(@"返回flutter端的数据");
}];
// 触发事件执行
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterBasicMessageChannel* messageChannel2 = [FlutterBasicMessageChannel messageChannelWithName:@"samples.flutter.io/message2" binaryMessenger:controller];
// 发送消息
[messageChannel2 sendMessage:(@"发送给flutter的数据") reply:^(id reply) {
NSLog(reply);
}];
Android
BasicMessageChannel<Object> messageChannel = new BasicMessageChannel<Object>(getFlutterView(), "samples.flutter.io/message", StandardMessageCodec.INSTANCE);
// 接收消息监听
messageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() {
@Override
public void onMessage(Object o, BasicMessageChannel.Reply<Object> reply) {
System.out.println("onMessage: " + o);
reply.reply("返回给flutter的数据");
}
});
// 触发事件执行
BasicMessageChannel<Object> messageChannel2 = new BasicMessageChannel<Object>(getFlutterView(), "samples.flutter.io/message2", StandardMessageCodec.INSTANCE);
// 发送消息
messageChannel2.send("发送给flutter的数据", new BasicMessageChannel.Reply<Object>() {
@Override
public void reply(Object o) {
System.out.println("onReply: " + o);
}
});
MethodChannel
Flutter
static const platform = const MethodChannel('samples.flutter.io/battery');
Future<Null> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
// 执行_getBatteryLevel方法
iOS
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"samples.flutter.io/battery"
binaryMessenger:controller];
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// TODO
if ([@"getBatteryLevel" isEqualToString:call.method]) {
int batteryLevel = [self getBatteryLevel];
if (batteryLevel == -1) {
result([FlutterError errorWithCode:@"UNAVAILABLE"
message:@"Battery info unavailable"
details:nil]);
} else {
result(@(batteryLevel));
}
} else {
result(FlutterMethodNotImplemented);
}
}];
- (int)getBatteryLevel {
UIDevice* device = UIDevice.currentDevice;
device.batteryMonitoringEnabled = YES;
if (device.batteryState == UIDeviceBatteryStateUnknown) {
return -1;
} else {
return (int)(device.batteryLevel * 100);
}
}
Android
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// TODO
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
}
);
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
FlutterEventChannel
Flutter
static const EventChannel _eventChannel = const EventChannel('samples.flutter.io/test');
void _onEvent(Object event) {
print('返回的内容: $event');
}
void _onError(Object error) {
print('返回的错误');
}
@override
void initState() {
// TODO: implement initState
super.initState();
// 监听开始
_eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
iOS
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterEventChannel* eventChannel = [FlutterEventChannel eventChannelWithName:@"samples.flutter.io/test" binaryMessenger:controller];
[eventChannel setStreamHandler:self];
FlutterEventSink eventSink;
// // 这个onListen是Flutter端开始监听这个channel时的回调,第二个参数 EventSink是用来传数据的载体。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(FlutterEventSink)events {
eventSink = events;
// arguments flutter给native的参数
// 回调给flutter, 建议使用实例指向,因为该block可以使用多次
if (events) {
events(@"主动发送通知到flutter");
}
// 监听电池状态
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onBatteryStateDidChange:)
name:UIDeviceBatteryStateDidChangeNotification
object:nil];
return nil;
}
/// flutter不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
// arguments flutter给native的参数
[[NSNotificationCenter defaultCenter] removeObserver:self];
eventSink = nil;
return nil;
}
- (void)onBatteryStateDidChange:(NSNotification*)notification {
if (eventSink == nil) return;
UIDeviceBatteryState state = [[UIDevice currentDevice] batteryState];
switch (state) {
case UIDeviceBatteryStateFull:
case UIDeviceBatteryStateCharging:
eventSink(@"charging");
break;
case UIDeviceBatteryStateUnplugged:
eventSink(@"discharging");
break;
default:
eventSink([FlutterError errorWithCode:@"UNAVAILABLE"
message:@"Charging status unavailable"
details:nil]);
break;
}
}
Android
new EventChannel(getFlutterView(), CHANNEL2).setStreamHandler(
new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
this.eventSink = eventSink;
handler.sendEmptyMessageDelayed(1, 1000);
}
@Override
public void onCancel(Object o) {
}
private EventChannel.EventSink eventSink;
private int count = 0;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
eventSink.success((count++) + "主动发送消息给flutter");
// handler.sendEmptyMessageDelayed(1,1000);
}
};
}
);