likes
comments
collection
share

Flutter、iOS混合开发实践

作者站长头像
站长
· 阅读数 5

一、前言

上一篇笔记介绍了Flutter、Android混编的操作步骤,这篇笔记介绍一下iOSFlutter混编的应用。

阅读Flutter官方文档我们可以大致了解iOSFlutter混编的关键步骤,都需要将Flutter相关的文件编译成静态库framework,再通过CocoaPods进行管理。下面记录本人根据官方文档及网上优秀作者分享的便捷脚本使用过程。

二、新建Flutter模块并编译成framework

  • 步骤一:新建iOS工程,使用CocoaPods管理工程
  • 步骤二:新建Flutter Module
  • 步骤三:将Flutter Module编译成framework,引入iOS工程
  • 步骤四:编写测试代码,查看结果
老规矩在开始新建工程前先新建一个总文件(这里命名为iOS_Flutter_MixBuilder),请确保在安装了CocoaPods的前提下进行下面操作。

彩蛋:在目前版本中FlutterEngine携带的路由在Flutter中统一变为"/",应该算是Flutter一个bug,目前使用FlutterEngine创建的方式无法指定具体路由。

关键名词介绍:

FlutterViewControllerFlutter页面控制器,我们可以直接push/present到该控制器,或将其作为ChildViewController嵌入到我们的页面中。

FlutterEngineFlutter负责在iOS端执行Dart代码的引擎,将Flutter编写的UI代码渲染到FlutterViewController中。

接下来我们在FirstNativeViewController中打开Flutter页面,并完成其和原生之间的通信。为了方便我们将FirstNativeViewController定义为全局属性

@property (nonatomic, strong) FlutterViewController *flutterViewController;
不使用FlutterEngine打开Flutter页面代码如下:

//初始化FlutterViewController
self.flutterViewController = [[FlutterViewController alloc] init];
//为FlutterViewController指定路由以及路由携带的参数
[self.flutterViewController setInitialRoute:@"route1?{\"message\":\"嗨,本文案来自第一个原生页面,将在Flutter页面看到我\"}"];
//设置模态跳转满屏显示
self.flutterViewController.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:self.flutterViewController animated:YES completion:nil];

使用FlutterEngine打开Flutter页面代码如下:

//初始化FlutterEngine
FlutterEngine *flutterEngine = [[FlutterEngine alloc]initWithName:@"FirstFlutterViewController"];
//指定路由打开某一页面,Flutter1.12版本指定路由后在Flutter代码里获取的路由统一为“/”,为Flutter bug
[[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute" arguments:@"route1?{\"message\":\"嗨,本文案来自第一个原生页面,将在Flutter页面看到我\"}"];
//路由的指定需要在FlutterEngine run方法之前,run方法之后指定路由不管用
[flutterEngine run];
//使用FlutterEngine初始化FlutterViewController
self.flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
//设置模态跳转满屏显示
self.flutterViewController.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:self.flutterViewController animated:YES completion:nil];

FlutterDart代码如下:

解析路由获取本次携带的数据

void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String url) {
  // route名称
  String route =  url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
// 参数Json字符串
  String paramsJson =  url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
  Map<String, dynamic> mapJson = json.decode(paramsJson);  String message = mapJson["message"];
// 解析参数
  switch (route) {
    case 'route1':
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter页面'),
          ),
          body: Center(child: Text('页面名字: $route',style: TextStyle(color: Colors.red), textDirection: TextDirection.ltr),),
        ),
      );
    default:
      return Center(
        child: Text('Unknown route: $route',style: TextStyle(color: Colors.red), textDirection: TextDirection.ltr),
      );
  }}

完成以上代码就可以在FirstNativeViewController中打开Flutter页面,下面介绍iOSFlutter是如何交互的:

思路:我们熟悉的传统的h5页面和原生交互时,通过中间通信工具对象,定义好方法或者属性进行通信。同理,FlutteriOS原生交互也有专门的通信对象(Platform Channel),它有三种类型:

  • MethodChannel:用于最常见的方法传递,帮助Flutter和原生平台互相调用方法,也是本次我们着重介绍的。
  • BasicMessageChannel:用于数据信息的传递。
  • EventChannel:用于事件监听传递等场景

在上面的介绍中,我们可以在一个iOS页面中打开Flutter页面,那接下来我们只需要通过MethodChannelFlutter发送命令,以及接收消息的回调。那么我们就可以在iOSFlutter页面呈现一些对方传过来的数据。开整!

iOS部分代码如下(下面的代码依旧在FirstNativeViewController中编写):

我们在开始使用MethodChannel时,先对其进行唯一性定义。注意:这里我们定义两个MethodChannel,一个用于对Flutter的消息发送,一个用于Flutter的回调消息接收。

//Flutter向Native发消息static NSString *CHANNEL_NATIVE = @"com.example.flutter/native";
//Native向Flutter发消息static NSString *CHANNEL_FLUTTER = @"com.example.flutter/flutter";

使用定义好的名字,初始化MethodChannel注意:MethodChannel初始化方法里有两个参数。第一个参数BinaryMessenger messenger,我们可以理解为MethodChannelFlutter页面的绑定项,通过flutterViewController.binaryMessenger或者flutterEngine.binaryMessenger我们都可以可以得到构造MethodChannel的第一个参数。第二个参数需要传入我们之前定义好的唯一命名。

iOS接收Flutter发来的消息

接收Flutter消息得先初始化一个MethodChannel,且用之前定义好的名字CHANNEL_NATIVE。通过下面代码我们可以看到MethodChannel回调参数有:FlutterMethodCall callFlutterResult resultcall可以给我们提供本次Flutter所发送的方法名(call.method),还可以提供本次Flutter所发送的方法携带的参数(call.arguments)。result是一个block回调我们在处理完逻辑后可以调用这个block告知Flutter我们的结果。

//初始化messageChannel,CHANNEL_NATIVE为iOS和Flutter两端统一的通信信号
FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NATIVE binaryMessenger:self.flutterViewController.binaryMessenger];
//接受Flutter回调
[messageChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        if ([call.method isEqualToString:@"openSecondNative"]) {
            //打开第二个原生页面
            NSLog(@"打开第二个原生页面");
            strongSelf.sMessageFromFlutter = call.arguments[@"message"];
            [strongSelf pushSecondNative];
            //告诉Flutter我们的处理结果
            if (result) {
                result(@"成功打开第二个原生页面");
            }
        }
        else if ([call.method isEqualToString:@"backFirstNative"]){
            //返回第一个原生页面
            NSLog(@"返回第一个原生页面");
            [strongSelf backFirstNative];
            strongSelf.lblTitle.text = call.arguments[@"message"];
            //告诉Flutter我们的处理结果
            if (result) {
                result(@"成功返回第一个原生页面");
            }
        }
    }];

//打开第二个原生页面
- (void)pushSecondNative{
    SecondNativeViewController *secondNativeVC = [[SecondNativeViewController alloc]initWithNibName:@"SecondNativeViewController" bundle:nil];
    secondNativeVC.showMessage = self.sMessageFromFlutter;
    __weak __typeof(self) weakSelf = self;
    //第二个原生页面的block回调
    secondNativeVC.ReturnStrBlock = ^(NSString *message){
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        //从第二个原生页面回来后通知Flutter页面更新文案
        [strongSelf sendMessageToFlutter:message];
    };
    secondNativeVC.modalPresentationStyle = UIModalPresentationFullScreen;
    //进行本操作时,当前屏幕的s控制器为FlutterViewController,所以应该使用self.flutterViewController进行跳转
    [self.flutterViewController presentViewController:secondNativeVC animated:YES completion:nil];
}

- (void)backFirstNative{
    //关闭Flutter页面
    [self.flutterViewController dismissViewControllerAnimated: YES completion: nil];
}

注意:例子中iOS涉及ViewController之间回调统一使用block来处理(例如:ReturnStrBlock)。

iOS给Flutter发消息

Flutter发消息同样得先初始化一个MethodChannel,且用之前定义好的名字CHANNEL_FLUTTER。使用MethodChannel的方法invokeMethod就可以将本次的消息发送到Flutter中去啦!

- (void)sendMessageToFlutter:(NSString *)message{
    //初始化messageChannel,CHANNEL_FLUTTER为iOS和Flutter两端统一的通信信号
    FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:CHANNEL_FLUTTER binaryMessenger:self.flutterViewController.binaryMessenger];
    [messageChannel invokeMethod:@"onActivityResult" arguments:@{@"message":message}];}

上面介绍了交互时iOS端的代码,下面介绍Flutter端的代码。如下:

首先我们在原来的main.dart文件中做一下扩展。定义一个Widget用来显示iOS传过来的数据,并创建一个按钮给iOS发消息。同iOS端,在main.dart文件中我们也定义了同名MethodChannel。注意:我们在WidgetinitState()方法里就应该写上MethodChannel的监听代码。我们可以在FlutterMethodChannel的回调方法中通过获取call.method、call.method.arguments来知道,iOS这次想要调用我们什么方法、以及带来了什么参数。

class ContentWidget extends StatefulWidget{
  ContentWidget({Key key, this.route,this.message}) : super(key: key);
  String route,message;
  _ContentWidgetState createState() => new _ContentWidgetState();
}
class _ContentWidgetState extends State<ContentWidget>{
  static const nativeChannel = const MethodChannel('com.example.flutter/native');
  static const flutterChannel = const MethodChannel('com.example.flutter/flutter');
  void onDataChange(val) {
    setState(() {
      widget.message = val;
    });
  }
  @override
  void initState(){
    super.initState();
    Future<dynamic> handler(MethodCall call) async{
      switch (call.method){
        case 'onActivityResult':
          onDataChange(call.arguments['message']);
          print('1234'+call.arguments['message']);
          break;
      }
    }
    flutterChannel.setMethodCallHandler(handler);
  }
  Widget build(BuildContext context) {
    // TODO: implement build
    return Center(
      child: Stack(
        children: <Widget>[
          Positioned(
            top: 100,
            left: 0,
            right: 0,
            height: 100,
            child: Text(widget.message,textAlign: TextAlign.center,),
          ),
          Positioned(
            top: 300,
            left: 100,
            right: 100,
            height: 100,
            child: RaisedButton(
                child: Text('打开上一个原生页面'),
                onPressed: (){
                  returnLastNativePage(nativeChannel);
                }
            ),
          ),
          Positioned(
            top: 430,
            left: 100,
            right: 100,
            height: 100,
            child: RaisedButton(
                child: Text('打开下一个原生页面'),
                onPressed: (){
                  openNextNativePage(nativeChannel);
                }
            ),
          )
        ],
      ),
    );
  }}

上面的代码缺少了方法:returnLastNativePageopenNextNativePage。如下:

大家肯定还记得我们之前在iOS页面接收Flutter的回调后,还能调用result这个block来告诉Flutter页面我们的处理结果。没错,我们在下面两个方法中,异步获取这些回调的信息并打印。

Future<Null> returnLastNativePage(MethodChannel channel) async{
  Map<String, dynamic> para = {'message':'嗨,本文案来自Flutter页面,回到第一个原生页面将看到我'};
  final String result = await channel.invokeMethod('backFirstNative',para);
  print('这是在flutter中打印的'+ result);
}

Future<Null> openNextNativePage(MethodChannel channel) async{
  Map<String, dynamic> para = {'message':'嗨,本文案来自Flutter页面,打开第二个原生页面将看到我'};
  final String result = await channel.invokeMethod('openSecondNative',para);
  print('这是在flutter中打印的'+ result);
}

至此,iOSFlutter可以互通有无了。如果你在编译的时候发现main.dartMethodChannel报错,那么你一定是没有正确的引入头文件比如:import 'package:flutter/services.dart'

注意:在你变更Flutter文件内容后,记得重新运行上面介绍的脚本文件build_file.sh,并切到ios_app文件夹目录下重新pod install一下更新Pod库哦。

上面的尝试都是基于Flutter1.12版本实现,若您的Flutter版本 < 1.12,请先更新Flutter版本。

-----------------------------------完整代码地址------------------------------------------------

功能代码地址:

https://github.com/JJwow/iOS_Flutter_MixBuilder.git

Pod库地址:

https://github.com/JJwow/flutter_lib.git

转载自:https://juejin.cn/post/6844904087918477320
评论
请登录