Flutter 与Native原生交互

5,190 阅读7分钟

学习Flutter也有一段时间了,今天来介绍一下Flutter是如何与原生交互的。

原生交互的重要性就不用说了吧。毕竟Flutter也不是万能的,有时候还是需要咱们原生的支持,才能达成各种奇奇怪怪的需求,那么话不多说,直接开干。

1. 新建一个Flutter工程

这次我们的目的是与原生交互,那么创建方式自然与先前不同 之前选择的是 Flutter Application 普通工程 这次我们选择 Flutter Module 交互工程

从上图可以看出,刚创建出来的工程,与普通的Flutter Application不同 androidios文件夹的名称前面都多了个.,在本地文件夹中查看带 .的文件夹可以发现这两个文件夹是隐藏文件夹。

那么为什么要把这两个文件夹隐藏起来呢? 答:这两个文件夹的内容与普通的Flutter Application一样,但是这两个工程只是用来给测试的,不参与到原生交互当中。 所谓的测试就是我们在Android Studio中Run一个项目。

2. Xcode新建一个Native工程

创建一个Xcode项目与Flutter项目同一路径,如下图

3. 添加依赖

3.1 cocoapods 引入Flutter支持
  1. $cd Native工程路径
  2. $pod init
  3. 编辑 podFile 内容 (内容在下面,注意看中文内容)
  4. 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 写代码
  1. 我在Main.storyboard中创建了个按钮,并将其点击事件拖到 ViewController.m 中。
  2. 声明了一个FlutterViewController属性变量
  3. 在点击事件中,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),
        ) ,
      )
    );
  }
}

效果如下:

image.png

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 测试通道是否连通

image.png

如上,我们已经成功获取了Flutter端传递给Native端的数据。 接下来,我们再试试Native如何通过MethodChannel传递参数给Flutter。

#####三、Native传递数据给Flutter

这里我们需要修改的内容比较多,请耐心看。

  1. Flutter创建MethodChannel
  final MethodChannel _channerOne = MethodChannel('pageOne');
  final MethodChannel _channerTwo = MethodChannel('pageTwo');
  final MethodChannel _channerDefault = MethodChannel('pageDefault');
  1. 设置变量获取界面名称以及初始化判断
  var _pageName = '';
  var _initialized = false;
  1. 通道设置通道回调
   @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('这是默认'),
                ),
              ) ,
            )
        );
      }
    }
  }
  1. 回到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];
}
  1. 交互效果
    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. 交互结果

image.png

###结语 那么以上就是本次 Flutter 与 Native 原生交互的全部内容。当然 这里还有另外一种交互方式,EventChannel传递数据流,下次有机会的话,在给大家补齐还请见谅。