likes
comments
collection
share

flutter-与ios原生交互

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

前言

flutter开发也不可能纯flutter开发,除非只开发一个图片文本浏览的工具,否则一旦用到原生的,那么就需要了解原生flutter的交互了

交互场景很多,例如:原项目以原生为主,要增加flutter作为部分功能,如订单模块;项目以flutter为主,但是使用到了原生的硬件相关,还没有合适的三方,需要通过原生处理

因此 flutter原生之间同时进行开发的情况不在少数,这篇文件主要介绍 flutterios的交互,android的也类似,只要大致交互逻辑明白即可

测试案例demo

ps:这里一共介绍两种情况,一种原生为主、flutter为辅的(初始页面使用原生编写),一种flutter为主、原生为辅,如果功能差不多看自己选择了,本质差不多

另外实际使用过程中,完全以自己项目为基准,下面两种方案只是初期配置方式不一样,要是自己肯改动,其实原理都一样的(要是两个都看完相信很快理解)

module(原生为主、flutter为辅)

本方案主要采用module方式配置flutter,这种情况一般是原生为主、flutter为辅,即:flutter编写项目主要应用于某个功能模块,例如:订单、会员、个人信息、活动等,默认从原生开始首页等功能

:该方式解耦后,flutter项目和原生就分开了

另外此方式如果想运行完整项目,必须要从 xcode 或者 android studio开始运行,flutter中直接运行的仅仅是flutter单个模块的功能(走的是另外一个默认原生工程,后面会介绍,是不是很像测试那套😂)

创建flutter module

首先创建一个 fluttermodule 模块,选择了之后,会发现跟原生编程语言没关系,如下所示(记得项目名字不要大写)

flutter-与ios原生交互

创建完成之后,会发现,创建的默认的app不一样,androidios的都是.开头的隐藏文件

flutter-与ios原生交互

另外,.ios隐藏文件夹的 Generated.xcconfig后面要是报错可能会需要了解他,如下所示

flutter-与ios原生交互

然后就开始配置原生了!

配置原生

创建一个 ios 原生项目,和 flutter一个目录,如下所示(也可以不一个目录,但更新path即可),

flutter-与ios原生交互

1、配置Podfile

然后编写 Podfile文件,如果没有 pod init初始化,加入内容如下所示

platform :ios, '9.0'

target 'FlutterObjcDemo' do
  use_frameworks!

  # 导入flutter相关环境
  # 路径,../表示前一个目录,./表示当前目录
  flutter_application_path = '../flutter_module/'
  
  # 加载环境
  load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  install_all_flutter_pods(flutter_application_path)
  
  #  下面这个也可以加载环境
  # eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

end

编辑完成后,然后 pod install 即可

2、设置 bitcode

然后 Build Settingbitcode 设置为 no

flutter-与ios原生交互

注意:到这里就已经设置差不多了,项目也不会报错,但会有一个问题,那就是项目只能加载flutter编译好的内容,当flutter更新文件,却没有重新编译时,那么 xcode运行的会是flutter上次编译的缓存,这就很苦恼

3、脚本文件设置

1、拖拽 Generated.xcconfig文件到项目

执行脚本文件之前,我们往项目中拖拽前面提到的.iosFlutter文件夹,如下所示,不要copy

flutter-与ios原生交互

然后右键移除多余的文件引用(选择Remove References,不要移动到废纸篓)选择只留下一个,如下所示

flutter-与ios原生交互

:这个步骤是自己尝试出来的,看别人的没有提到相对路径无法识别的问题😂

2、添加 run Script 脚本

庆幸的是,flutter早在tools中给我们提供了一个xcode_backend.sh的脚本文件,我们只需要设置好脚本文件,即可在项目运行时自动编译flutter项目

然后在 Build Phases 中添加一个脚本执行模块

flutter-与ios原生交互

然后,在下面位置添加脚本,且Run Script的位置,要排在第二位,要在xcode编译之前,先执行脚本编译flutter项目,否则运行会出现问题(注意:如果没有此脚本,flutter项目不会编译更新,只会用缓存)

flutter-与ios原生交互

具体脚本内容就如下所示($FLUTTER_ROOTflutter的环境相对目录),复制进去就行,然后编辑运行成功就配置完毕了(前面往项目中拖拽Generated.xcconfig就是为了这里,有些机器环境就是不识别),运行失败看下面一步

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

备用:由于环境配置或者其他未知问题,有些电脑可能还是无法识别 $FLUTTER_ROOT命令,因此会出错,之前提到过Generated.xcconfig,这个文件里面有这个绝对路径信息,我们将他们复制替换过来就行,如下所示(反正我是同样的配置方式,一台电脑可以,而另一台不可以😂)

"/Users/lishuai/flutter/packages/flutter_tools/bin/xcode_backend.sh" build
"/Users/lishuai/flutter/packages/flutter_tools/bin/xcode_backend.sh" embed

两端交互

这里先说明一下,开启 flutter 模块,实际上就是创建了一个 FlutterViewController控制器,然后运行,如果想启动运行 Flutter模块(和默认创建的Flutter项目一样),那么直接设置 rootViewController即可,否则就像一个正常的控制器一样,进行push或者present

:虽然下面的案例也是设置rootViewController,但实际运行过程中不推荐这样使用,这样使用会影响flutterios端应用声明周期的监听,如果想监听后续会了解到

ios端交互设置

Object-c 为例,在 ios 我们需要引入头文件 Flutter.h,如下所示

#import <Flutter/Flutter.h>

然后创建 FlutterViewController,通过 FlutterMethodChannel 进行交互

//创建flutter模块寄托的 ViewController
FlutterViewController *vc = [[FlutterViewController alloc] init];

//可以直接设置成根控制器,也可以进行push或者dismiss,这里设置成根控制器
self.window.rootViewController = vc;

//设置 methodChannel 用于和 flutter 通信
//如果原生端组件化开发,那就每个组件创建各自的methodChannel即可,分别对应特定功能
//可以创建多个methodChannel变量,即:起不同的名字,可以分别和flutter不同的功能进行通信
//例如:一个订单页面通信,一个和个人页面通信
self.methodChannel = 
    [FlutterMethodChannel methodChannelWithName:@"mine/mine_page" binaryMessenger:vc];

//向flutter端以method发送一个消息
//第一个参数为方法名,第二个为参数,参数多的话建议json字符串,两边分别解析
[self.methodChannel invokeMethod:@"one" arguments:nil];
//最后一个参数是发送回调,如果fluter端出现了问题,可以通过回调得到回馈,否则就是成功,一般不会出错
[self.methodChannel invokeMethod:@"one" arguments:nil result:^(id  _Nullable result) {}];

//监听flutter发送过来的消息
[self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call,
    FlutterResult  _Nonnull result) {
    //接收到flutter传递过来的消息
    //可以看到 call 有 method 和 arguments两个参数,分别为方法名,参数
    NSLog(@"ios回调 %@ -- %@", call.method, call.arguments);
    //如果交互过程出现了问题,可以调用 result 回调,将内容回调给 flutter端
}];

如果想在运行flutter时立即传递一个参数,则通过下面方式传递创建 FlutterViewController即可

FlutterViewController *vc = [[FlutterViewController alloc] 
    initWithProject:nil initialRoute:@"one" nibName:nil bundle:nil];

flutter端交互设置

如下所示,flutter 中直接声明MethodChannel对象和ios端进行通信(android也是一套逻辑),这里没有先引用头文件,相信开发过的会马上明白,报错提示直接自动纠正就引入了 flutter/services 文件

//头文件
import 'package:flutter/services.dart';

//声明 MethodChannel
里面名称需要唯一化:可以功能模块名字/功能名字,也可以像官方提示一样com... 保证不重复即可
final MethodChannel _channel = const MethodChannel("mine/mine_page");

iosandroid端发送消息

//和ios端设置一样,第一个参数方法名,第二个为参数
//结果返回一个 Future,如果交互过程出现了问题,可以及时接收反馈结果,和原生端一样
_channel.invokeMethod("testInfo");
_channel.invokeMethod("testInfo", "我是参数");

监听iosandroid端发来的消息

//设置回调
_channel.setMethodCallHandler((MethodCall call) {
  print(call.method);
  print(call.arguments);
  print("flutter--回调结果");
  //这个参数用于反馈给原生端,如果运行出现了问题,那边能通过 callback 监听到回馈
  return Future(() => null); //给一个空Future
});

这样两个端的交互问题解决了

module 单独调试

flutter项目的热更在开发中很节约时间,上面一直用原生运行开发是不现实的,实际上需要整个项目联调的时候才需要从原生端运行

当像单独调试 fluttermodule是,很简单,就像我们运行单独的 flutter项目一样,直接点击运行即可,他会自动以默认的备用空原生项目运行,同时加载 module内容(还记得前面的 .ios隐藏文件么,那可不是一个摆设)

就这样,将 module 当成单个 app运行了,自己可以根据调试内容,设置开始的参数,这式联调的时候去掉测试数据即可

application(flutter为主、原生为辅)

我们新创建一个demo一般就是这种,一般flutter为主、原生为辅

默认创建的是一个 flutter 项目,如下所示,不多说

flutter-与ios原生交互

然后 flutter 不需要我们配置,原生端呢,其实默认也是配置好的,其他也不需要我们配置

还记得前面讲的 Flutter模块其实就是开启了一个 FlutterViewController,其实就是一个控制器,开始的时候,系统会默认将跟控制器设置成 FlutterViewController,其实和我们的 Module 类似,且 bitcode脚本都设置完毕了,和 module基本类似,

:项目在xcode运行和flutter中运行结果一模一样

不信你看这个,刚出创建的项目默认加载 main.storyboard,其中跟控制器就是 FlutterViewController

flutter-与ios原生交互

是不是感觉,天哪,前面我们做了那么多,干嘛这么费劲,没事,还是有区别的,我们先看看效果,后面对比介绍

如下所示,会发现,他默认继承了 FlutterAppDelegate,这个平时我们也用不到,modules可以不用继承,默认的项目继承我们也不用管,改回来也没事

//FlutterAppDelegate的介绍说明,一般用于测试阶段,例如:modules根本不需要,可以根据自己需要重写回调
/**
 * `UIApplicationDelegate` subclass for simple apps that want default behavior.
 *
 * This class implements the following behaviors:
 *   * Status bar touches are forwarded to the key window's root view
 *     `FlutterViewController`, in order to trigger scroll to top.
 *   * Keeps the Flutter connection open in debug mode when the phone screen
 *     locks.
 *
 * App delegates for Flutter applications are *not* required to inherit from
 * this class. Developers of custom app delegate classes should copy and paste
 * code as necessary from FlutterAppDelegate.mm.
 */

@interface AppDelegate : FlutterAppDelegate

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
      [GeneratedPluginRegistrant registerWithRegistry:self];
      [self setupFlutter];
      // Override point for customization after application launch.
      return [super application:application 
          didFinishLaunchingWithOptions:launchOptions];
}

flutter和ios交互

交互不多说了,和ios一样,可以抽离出来,就是因为以 flutter为主,导致目前只能写到 AppDelegate中了,可以统一放到一起整理起来

- (void)setupFlutter {
    UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController;
    _methodChannel =  [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:(FlutterViewController *)vc];
    [_methodChannel invokeMethod:@"one" arguments:nil];
    //监听退出
    [_methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        NSLog(@"ios回调 %@ -- %@", call.method, call.arguments);
    }];
}

两种方式区别,以及推荐场景

区别

通过 module创建的项目:

flutter的模块可以单独运行,不受原生端代码的影响,相对比较独立,当然要通过 flutter端运行(一般在 android studio),需要整体运行的时候,通过原生端启动即可;

特点flutter端调试方便,减少了原生项目的编译时间,仍然能热更,减少了原生项目的影响,但测试容易遗漏与原生端的沟通问题

通过 application创建的项目:

flutter模块不可以单独运行,项目运行的时候会自动先编译flutter在编译原生运行,因此每次都要编译两个端,每次都是运行整个项目;

特点:调试过程中总是针对整体代码,但flutter仍然可以热更,测试过程中不会遗漏功能,就是每次重新启动都要重新编译整个项目,容易受到原生项目的错误影响

推荐场景

module比较适用于成熟项目,原生端和 flutter 分离明显,希望他们之间最小影响,更适用于已经用原生开发的项目,后续直接接入module,开发过程对原生影响也不大

application比较适合小公司项目或者主要使用 flutter编写的项目,他们之间耦合略严重,但也不是不能分离

可以根据当前场景抉择,当然这只是一部分考量,实际中心偏移也没问题,根据自己项目来选择更快的方式

最后

本篇文章可以作为一个参考,开始你的flutter之旅吧