Flutter、iOS混合开发实践

·  阅读 8393

一、前言

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

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

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

阅读过上一篇《Flutter、Android混合开发实践》的同学知道,在AndroidFlutter未编译成aar前就可以进行引用调试。不同于安卓,iOS只能以framework的形态接入Flutter模块。大致步骤如下:

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


步骤一:新建iOS工程,使用CocoaPods管理工程

iOS_Flutter_MixBuilder目录下新建Xcode project,打开Xcode选择Create a new Xcode project -> 选择Single View App -> 将工程命名为ios_app -> 选择刚刚新建的文件夹(iOS_Flutter_MixBuilder),create





启动终端在iOS_Flutter_MixBuilder/ios_app目录下依次执行一下命令:

pod init
pod install
复制代码


步骤二:新建Flutter Module

打开终端切换到iOS_Flutter_MixBuilder目录下,执行下面命令:

flutter create -t module my_flutter复制代码

my_fluttermodule的名字,执行完命令等待即可。


此时我们观察一下Flutter Module文件目录,首先显示隐藏文件(macOS Sierra及以上(Mojave),我们可以使用快捷键 ⌘⇧.(Command + Shift + .) 来快速(在 Finder 中)显示和隐藏隐藏文件


隐藏文件夹.ios内文件不需要更改,每次flutter cleanflutter packages get操作后会重新生成。给Flutter Module添加任意插件引用后执行

flutter pub get后会生成一个Podfile文件。

打开pubspec.yaml文件,该文件管理Flutter插件的依赖。添加数据持久化插件依赖,保存文件。终端切换到my_flutter目录下,执行下面命令:

cd /Users/admin/Desktop/WJJ_WJJ/iOS_Flutter_MixBuilder/my_flutter复制代码

flutter pub get复制代码




注意:为了防止没有科学上网引起flutter pub get操作经常卡死,我们可以更换中国镜像https://flutter.io/community/china,更换操作:

vi ~/.bash_profile 复制代码

然后添加

export PUB_HOSTED_URL=https://pub.flutter-io.cn复制代码

export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn复制代码

然后保存,重启终端。

有的同学会发现重启终端执行任何Flutter命令都提示Waiting for another flutter command to release the startup lock...,在你的Flutter包中删除flutter/bin/cache/lockfile文件即可


步骤三:将Flutter Module编译成framework,引入iOS工程

这里有两种方式:

  • 方式一:在iOS工程的podfile文件中添加如下代码,my_flutter为Flutter Module名

flutter_application_path = '../my_flutter/'

load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

install_all_flutter_pods(flutter_application_path)
复制代码


然后切到iOS工程目录ios_app,执行:

pod install复制代码

打开iOS工程可以看到,Flutter相关的文件已被引入iOS工程中。


该方法好处是一步到位且是官方推荐的第一种方式,但需要每个引用framework的开发者电脑都有Flutter环境,且需要引用的framework分布在不同的文件夹里,查看繁琐,很显然这不友好。

  • 方式二:将Flutter Module编译产物通过CocoaPods引入工程

先对Flutter Module进行编译,选择debug模式或者release模式。注意:应添加--no-codesign防止证书引起的编译不通过。

终端切到my_flutter文件夹下执行下面命令(根据需要二选一):

flutter build ios --debug --no-codesign //编译debug产物(选择不需要证书)复制代码
flutter build ios --release --no-codesign //编译release产物(选择不需要证书)复制代码

命令执行成功后观察文件夹变化,build/ios/Debug-iphoneos文件夹存放着编译产物FlutterPluginRegistrant.frameworkshared_preferences.framework


上面的两个frameworkFlutter代码编译产物,除此之外我们还需要包含Flutter资源的库App.framework它在.ios/Flutter/目录下,以及Flutterengine运行库Flutter.framework它在.ios/Flutter/engine/目录下。

因为CocoaPods无法直接管理framework文件,所以我们需要把上面找到的四个文件简单的封成一个Pod库。

终端切到iOS_Flutter_MixBuilder文件夹下执行下面命令,回答一波灵魂拷问后,建立名为flutter_libPod库:

pod lib create flutter_lib复制代码


新建Pod库成功后会自动弹出例子工程,关掉它,观察文件目录。


简单介绍一下Pod库,flutter_lib.podspec文件是Pod库的配置文件,在该文件里可以指定Pod库的版本、以及库内容文件的地址等信息。


现在我们只需要把先前得到的四个Flutter库文件放到/flutter_lib/目录下的一个新文件夹ios_frameworks中,再在flutter_lib.podspec文件中指定ios_frameworks的路径就大功告成了。手动效率太低,这里借鉴了作者:做人要简单的脚本进行自动化处理。


将脚本build_file.sh放入my_flutter文件夹


终端切到my_flutter文件夹下运行下面命令使脚本工作:

sh build_file.sh复制代码


脚本运行成功后查看文件目录,我们所需要的framework已经出现在指定位置。


flutter_lib/ios_frameworks文件夹下的四个frameworkflutter_lib.podspec文件中需要被申明指定,flutter_lib.podspec中代码如下:


至此,本次所要制作的Flutter framework Pod库已经完成,下面只需要在iOS工程中将这个Pod库以本地的形式引入即可。打开iOS_Flutter_MixBuilder/ios_app/Podfile文件添加下面代码:

pod 'flutter_lib', :path => '../flutter_lib'复制代码


切到iOS_Flutter_MixBuilder/ios_app目录下执行Pod指令:pod install


指令成功后打开iOS工程查看,此时我们所制作Pod库已经成功被引入iOS工程,build一下iOS工程,成功!



步骤四:编写测试代码,查看结果

我们沿用安卓篇使用的Flutter代码,同时尝试安卓篇列举的4个经典场景:

  • iOS页面打开Flutter页面并传值
  • Flutter页面打开iOS页面并传值
  • iOS页面退回Flutter页面并传值
  • Flutter页面退回iOS页面并传值

iOS中我们需要新建的页面有原生页面FirstNativeViewController、SecondNativeViewController和安卓中不同,我们不需要新建套壳原生页面FirstFlutterActivity。因为iOS中我们可以实例化Flutter控制器对象:FlutterViewController。而安卓中是实例化了Flutter视图:FlutterView,FlutterView需要一个壳子控制器FirstFlutterActivity去承载它

在开始上面的场景前,Flutter页面定义如下内容:

  1. 添加一个textView用来显示其他页面传过来的内容
  2. 添加一个button用来打开下个原生页面
  3. 添加一个button用来返回到上个原生页面

iOS中显示Flutter页面有两种思路:

  1. 实例化一个FlutterViewController对象 -> 为FlutterViewController指定路由 -> push/presentFlutterViewController
  2. 实例化一个FlutterEngine -> 为FlutterEngine指定路由 -> FlutterEngine run -> 使用FlutterEngine创建一个FlutterViewController -> push/presentFlutterViewController  

注意:方法2中,为FlutterEngine指定路由必须在FlutterEngine run之前,否则路由无效。


彩蛋:在目前版本中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复制代码



分类:
iOS
标签:
分类:
iOS
标签: