学习Flutter也有一段时间了,今天来介绍一下Flutter是如何与原生交互的。
原生交互的重要性就不用说了吧。毕竟Flutter也不是万能的,有时候还是需要咱们原生的支持,才能达成各种奇奇怪怪的需求,那么话不多说,直接开干。
1. 新建一个Flutter工程
这次我们的目的是与原生交互,那么创建方式自然与先前不同
之前选择的是 Flutter Application
普通工程
这次我们选择 Flutter Module
交互工程
从上图可以看出,刚创建出来的工程,与普通的Flutter Application
不同 android
与 ios
文件夹的名称前面都多了个.
,在本地文件夹中查看带 .
的文件夹可以发现这两个文件夹是隐藏文件夹。
隐藏文件夹
那么为什么要把这两个文件夹隐藏起来呢?
答:这两个文件夹的内容与普通的Flutter Application
一样,但是这两个工程只是用来给测试的,不参与到原生交互当中。
所谓的测试就是我们在Android Studio中Run一个项目。
2. Xcode新建一个Native工程
创建一个Xcode项目与Flutter项目同一路径,如下图
3. 添加依赖
3.1 cocoapods 引入Flutter支持
- $cd Native工程路径
- $pod init
- 编辑 podFile 内容 (内容在下面,注意看中文内容)
- pod install
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'FlutterNative' do
flutter_application_path = '../你的flutter文件夹/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
end
3.2 编译Native内容
- 打开Native文件夹中的
.xcworkspace
- 关闭Bitcode
- 编译一下工程,一般情况都会
Build Success
3.3 设置Flutter编译脚本
-
TARGET -> Build Phases -> 添加脚本
image.png - 输入脚本内容(内容如下)
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
脚本内容可以在对应路径下找到,有兴趣的同学可以自行翻阅。
-
移动脚本编译位置
因为Build Success
是有编译顺序的,为了避免一些不必要的情况。
按下图操作。
image.png command + B 跑一下
3.4 写代码
- 我在
Main.storyboard
中创建了个按钮,并将其点击事件拖到ViewController.m
中。 - 声明了一个FlutterViewController属性变量
- 在点击事件中,present这个VC
tips: FlutterViewController无需重复创建,一旦加载之后,在程序运行期间将永!不!释!放!,重复创建将会导致内存占用越来越大。
#import "ViewController.h"
#import <Flutter/Flutter.h>
@interface ViewController ()
@property(strong,nonatomic)FlutterViewController *flutterVC;
@end
@implementation ViewController
- (IBAction)FirstBtnClick:(id)sender {
self.flutterVC = [FlutterViewController new];
[self presentViewController:self.flutterVC animated:YES completion:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
3.5 运行程序(Xcode工程)
-
看到我们创建好的Flutter工程最初的样子
新Fluuter工程.png -
Flutter中尝试修改一下标题,重新运行Xcode工程
修改标题.png
至此,我们就完成了Flutter与Native原生交互的第一步!搭建好Flutter 与Native 原生之间的一道桥。
4. 开始交互
我们开始在上文代码的基础上,继续编写交互代码。
一、设置默认初始化路由页面
1. Xcode工程设置初始化路由
- (IBAction)FirstBtnClick:(id)sender {
self.flutterVC = [FlutterViewController new];
//设置初始化路由
[_flutterVC setInitialRoute:@"pageID"];
[self presentViewController:self.flutterVC animated:YES completion:nil];
}
2. Flutter工程中设置默认路由名称
2.1
import 'dart:ui';
2.2. 声明一个变量,接收Native发送过来的字符串final String pageIdentifier
;
2.3 Flutter打印接收到的内容
import 'package:flutter/material.dart';
import 'dart:ui';
//传入window.defaultRouteName
void main() => runApp(MyApp(pageIdentifier: window.defaultRouteName,));
class MyApp extends StatefulWidget {
//声明接收变量
final String pageIdentifier;
const MyApp({Key key, this.pageIdentifier}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
//打印widget.pageIdentifier
print(widget.pageIdentifier);
return MaterialApp(
home: Container(
color: Colors.white,
child: Center(
child: Text(widget.pageIdentifier),
) ,
)
);
}
}
效果如下:
3. 配置不同ID做不同的事
在Native端配置不同的setInitialRoute
,然后在Flutter端接收到之后,根据不同的ID,显示不同的页面。
示例代码如下:
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
print(widget.pageIdentifier);
switch(widget.pageIdentifier){
case 'pageA':{
return PageA();
}
case 'pageB':{
return PageB();
}
case 'pageC':{
return PageC();
}
defult:{
return DefalutPage();
}
}
}
}
警告:默认初始路由只能设置一次!后面反复设置时,不论传递的是什么,默认路由都是第一次进入时的那个。
原因是因为我们Flutter中,是用一个final修饰符修饰的变量接收,所以如果想换初始路由,我们需要重新创建一个FlutterViewController,但是这又是耗费内存。
所以,如果要跳转不同界面,还请继续往下看。
二、 Flutter传递数据给Native
这里我们创建在上文内容的基础上,给PageID加上一个点击事件,在点击PageID之后,退出Flutter界面,回到Native界面。
1. 引入服务
import 'package:flutter/services.dart';
2. MethodChannel
2.1 Flutter点击事件使用MethodChannel传递数据
child: GestureDetector(
onTap: () {
MethodChannel('test').invokeListMethod('dismiss','这里写参数');
},
child: Text(widget.pageIdentifier),
),
2.2 Native创建FlutterMethodChannel并设置回调
self.flutterVC = [FlutterViewController new];
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"test" binaryMessenger:self.flutterVC];
[channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
NSLog(@"%@ -- %@",call.method,call.arguments);
if ([call.method isEqualToString:@"dismiss"]) {
[self.flutterVC dismissViewControllerAnimated:YES completion:nil];
}
}];
2.3 测试通道是否连通
如上,我们已经成功获取了Flutter端传递给Native端的数据。
接下来,我们再试试Native如何通过MethodChannel传递参数给Flutter。
三、Native传递数据给Flutter
这里我们需要修改的内容比较多,请耐心看。
- Flutter创建MethodChannel
final MethodChannel _channerOne = MethodChannel('pageOne');
final MethodChannel _channerTwo = MethodChannel('pageTwo');
final MethodChannel _channerDefault = MethodChannel('pageDefault');
- 设置变量获取界面名称以及初始化判断
var _pageName = '';
var _initialized = false;
- 通道设置通道回调
@override
void initState() {
// TODO: implement initState
super.initState();
_channerOne.setMethodCallHandler((MethodCall call){
print('这是One接收原生的回调${call.method}==${call.arguments}');
_pageName = call.method;
setState(() {});
});
_channerTwo.setMethodCallHandler((MethodCall call){
print('这是Two接收原生的回调${call.method}==${call.arguments}');
_pageName = call.method;
setState(() {});
});
_channerDefault.setMethodCallHandler((MethodCall call){
print('这是Default接收原生的回调${call.method}==${call.arguments}');
_pageName = call.method;
setState(() {});
});
}
4.设置build方法
@override
Widget build(BuildContext context) {
//如果还没初始化那么判断规则就用初始化时传进来的ID
String switchValue = _initialized?_pageName:widget.pageIdentifier;
//标记已经初始化
_initialized = true;
switch(switchValue){
case 'pageOne':{
return MaterialApp(
home: Container(
color: Colors.white,
child: Center(
child: GestureDetector(
onTap: () {
_channerOne.invokeListMethod('dismissOne','这是通道一传回来的数据');
},
child: Text('这是第一页'),
),
) ,
)
);
}
case 'pageTwo':{
return MaterialApp(
home: Container(
color: Colors.white,
child: Center(
child: GestureDetector(
onTap: () {
_channerTwo.invokeListMethod('dismissTwo','这是通道二传回来的数据');
},
child: Text('这是第二页'),
),
) ,
)
);
}
default:{
return MaterialApp(
home: Container(
color: Colors.white,
child: Center(
child: GestureDetector(
onTap: () {
_channerDefault.invokeListMethod('dismissDefault','这是默认通道传回来的数据');
},
child: Text('这是默认'),
),
) ,
)
);
}
}
}
- 回到Native端
请注意看注释
@interface ViewController ()
{
//标记是否已经初始化过
BOOL isSettedInitialRoute;
}
@property(strong,nonatomic)FlutterViewController *flutterVC;
@property(strong,nonatomic)FlutterMethodChannel *channelOne;
@property(strong,nonatomic)FlutterMethodChannel *channelTwo;
@property(strong,nonatomic)FlutterMethodChannel *channelDefault;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//创建FlutterViewController(还没初始化,只有进入到Flutter页面后才算初始化完成)
self.flutterVC = [FlutterViewController new];
//初始化通道一
self.channelOne = [FlutterMethodChannel methodChannelWithName:@"pageOne" binaryMessenger:self.flutterVC];
__weak typeof(self) weakself = self;
//注册通道一回调
[_channelOne setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
NSLog(@"method:%@ -- arguments:%@",call.method,call.arguments);
if ([call.method isEqualToString:@"dismissOne"]) {
[weakself.flutterVC dismissViewControllerAnimated:YES completion:nil];
}
}];
//初始化通道二
self.channelTwo = [FlutterMethodChannel methodChannelWithName:@"pageTwo" binaryMessenger:self.flutterVC];
//注册通道二回调
[_channelTwo setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
NSLog(@"method:%@ -- arguments:%@",call.method,call.arguments);
if ([call.method isEqualToString:@"dismissTwo"]) {
[weakself.flutterVC dismissViewControllerAnimated:YES completion:nil];
}
}];
//初始化默认通道
self.channelDefault = [FlutterMethodChannel methodChannelWithName:@"pageDefault" binaryMessenger:self.flutterVC];
//注册默认通道回调
[_channelDefault setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
NSLog(@"method:%@ -- arguments:%@",call.method,call.arguments);
if ([call.method isEqualToString:@"dismissDefault"]) {
[weakself.flutterVC dismissViewControllerAnimated:YES completion:nil];
}
}];
}
- (IBAction)FirstBtnClick:(id)sender {
//如果还没初始化
if (!isSettedInitialRoute) {
//设置初始界面
[self.flutterVC setInitialRoute: @"pageOne"];
//标记已经初始化
isSettedInitialRoute = YES;
}else{
//如果已经初始化过了,就直接调用Flutter中注册的方法,
[self.channelOne invokeMethod:@"pageOne" arguments:@"iOS通过通道一发消息给Flutter"];
}
[self presentViewController:self.flutterVC animated:YES completion:nil];
}
- (IBAction)SecondBtnClick:(id)sender {
//这里就跟FirstBtnClick同理了
if (!isSettedInitialRoute) {
[self.flutterVC setInitialRoute: @"pageTwo"];
isSettedInitialRoute = YES;
}else{
[self.channelTwo invokeMethod:@"pageTwo" arguments:@"iOS通过通道二发消息给Flutter"];
}
[self presentViewController:self.flutterVC animated:YES completion:nil];
}
-
交互效果
image.png
上文我们利用MethodChannel在Flutter 与 Native 原生之间交互的内容,接下来我们继续了解一下另外一种Channel。
BasicMessageChannel
从字面意思呢,我们可以理解为这是一条为发送基础消息数据的通道。
他与MethodChannel有一些区别的地方。接下来就简单介绍一下这个BasicMessageChannel
1. Flutter端
1.1 创建通道
这里眼尖的同学会发现,这条通道比MethodChannel多了一个codec
参数,这个参数可以理解成"解码器"。这里我们用StandardMessageCodec()
final BasicMessageChannel _basicMessageChannel = BasicMessageChannel('basic', StandardMessageCodec());
1.2 注册方法回调
@override
void initState() {
// TODO: implement initState
super.initState();
_basicMessageChannel.setMessageHandler((message){
print('收到Xcode发来的消息 == $message');
});
}
1.3 调用消息通道给Native发送消息
这里由于代码比较长,省略无关紧要的部分
Container(
height: 80,
color: Colors.red,
child: TextField(
onChanged: (value){
// 将TextField文本内容发送给Native
_basicMessageChannel.send(value);
},
),
),
2. Native端
2.1 声明属性
@property(strong,nonatomic)FlutterBasicMessageChannel *basicChannel;
2.2 初始化消息通道
self.basicChannel = [FlutterBasicMessageChannel messageChannelWithName:@"basic" binaryMessenger:self.flutterVC];
2.3 注册方法回调
[_basicChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) {
NSLog(@"basicMessage == %@",message);
}];
2.4 Native给Flutter通过消息通道发消息
- (IBAction)BasicClick:(id)sender {
[self.basicChannel sendMessage:@"发一条消息给fultter"];
[self presentViewController:self.flutterVC animated:YES completion:nil];
}
3. 交互结果
结语
那么以上就是本次 Flutter 与 Native 原生交互的全部内容。当然 这里还有另外一种交互方式,EventChannel
传递数据流,下次有机会的话,在给大家补齐还请见谅。