22、Flutter - 混合开发(三)iOS原生调用Flutter

    技术2022-07-11  97

    混合开发(三)iOS原生调用Flutter

    Flutter 项目 调用一些原生的功能!用的比较多的就是第三方插件,因为比较简单

    官方 《Flutter实战》

    原生项目中部分页面使用Flutter,这种也是比较常见的。

    FLutter本身定位的是开发一个完整的App应用。所以要是只让其做成一个页面的话有些功能是不支持的。Flutter本身有自己的渲染引擎,如果是小项目用Flutter就不划算,只有非常大型的项目将其部分或者全部页面用Flutter来实现。

     

    详细代码参见Demo

    Demo地址 -> AiOSFlutterModule

     

    1、FLutter Module

    模块创建

    创建出来的工程看一下

    打开文件路径可以去看一下,是隐藏文件

    隐藏的目的是,官方不希望我们对这些文件进行操作。我们开发的是一个Flutter页面是能运行的,这里的ios 和 Android 文件只是为了让我们做测试用的。当我们把这个Flutter写好之后,是要集成到原生项目中使用的,并不需要那些隐藏的内容。

    扩展:

    打开终端输入命令:(由于系统不一样,有可能无效,可以自行上网查阅)

    1、显示隐藏文件/文件夹

    $ defaults write com.apple.finder AppleShowAllFiles -boolean true ; killall Finder

    2、隐藏隐藏文件/文件夹

    $ defaults write com.apple.finder AppleShowAllFiles -boolean false ; killall Finder

     

    2、新建iOS项目

    要让Flutter和我们的iOS项目产生管理,使用pod进行管理 先生成Podfiile 文件,直接打开终端,cd 打开iOS项目路径。pod init ,pod install。之后就可以再Xcode 里面查看了。

    liujilou@liujiloudeMacBook-Pro ~ % cd /Users/liujilou/Desktop/code/AiOSFlutterModule/NativeDemo liujilou@liujiloudeMacBook-Pro NativeDemo % pod init liujilou@liujiloudeMacBook-Pro NativeDemo % pod install

    然后用Xcode 打开工程,编辑Podfile 文件。这里的引用格式的Flutter官网提供的,可以去查阅

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

    platform :ios, '9.0'

    target 'NativeDemo' do   install_all_flutter_pods(flutter_application_path)   use_frameworks!   end

    注意:这个Flutter 的路径是相对路径,如果修改改了路径,这里记得要改变

    重新 pod install

    这样就关联成功了,一些 Flutter 的内容就安装进去了 这个时候我们也可以到ViewController 里面试一下是否成功了。 #import <Flutter/Flutter.h> 头文件能导入就说明成功了

     

    3、调用 Flutter 页面(一)不推荐

    iOS里面先创建2个按钮,然后去打开Flutter页面

    3.1、iOS页面

    - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; CGSize viewSize = self.view.frame.size; UIButton * button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button1.frame = CGRectMake((viewSize.width-100)/2, 100, 100, 40); button1.backgroundColor = [UIColor orangeColor]; button1.tag = 1001; [button1 setTitle:@"按钮一" forState:(UIControlStateNormal)]; [button1 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)]; [self.view addSubview:button1]; UIButton * button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button2.frame = CGRectMake((viewSize.width-100)/2, 200, 100, 40); button2.backgroundColor = [UIColor greenColor]; button2.tag = 1002; [button2 setTitle:@"按钮二" forState:(UIControlStateNormal)]; [button2 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)]; [self.view addSubview:button2]; } -(void)pushFlutter:(UIButton *)btn { NSString * pageIndex = @"one"; NSString * page = @"one_page"; if (btn.tag == 1002) { pageIndex = @"two"; page = @"two_page"; } // 不要每次都去alloc init 一个新的FLutter,这样非常消耗性能 FlutterViewController * vc = [[FlutterViewController alloc] init]; [vc setInitialRoute:pageIndex];//初始化传给Flutter的值 [self presentViewController:vc animated:YES completion:nil]; }

    我们这里用的是跟iOS一样的调用方式,每次点击按钮的时候去重新创建Flutter页面。 如果全屏显示Flutter页面,在Flutter中要做回退。我们先不做全屏显示,先演示后面做

    3.2、Flutter的页面

    import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() => runApp(MyApp( //window 需要导入import 'dart:ui'; //window.defaultRouteName 拿到的就是 iOS中写的 [vc setInitialRoute:@"one"];带过来的值 one pageIndex: window.defaultRouteName)); // ---------------------------------------------------- class MyApp extends StatelessWidget { final String pageIndex; const MyApp({Key key, this.pageIndex}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: rootPage(pageIndex), ); } rootPage(String pageIndex) { switch (pageIndex) { case 'one': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Center( child: RaisedButton( onPressed: () { // 直接退出页面了,一般不会这么做。 MethodChannel('one_page').invokeMapMethod('exit'); }, child: Text(pageIndex), ))); case 'two': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Center( child: RaisedButton( onPressed: () { MethodChannel('two_page').invokeMapMethod('exit'); }, child: Text(pageIndex), )), ); } } } iOS通过初始化的时候传值给Flutter   [vc setInitialRoute:pageIndex];Flutter通过  window.defaultRouteName  拿到iOS传过来的值,然后去判断 显示AppBar 等。然后 可以通过  MethodChannel('one_page').invokeMapMethod('exit');   退出Flutter页面,并给iOS发送消息传值。

     

    但是Flutter 创建之后是一直存在在内存中的,而且非常大

    通过上图可以看到,一个空的Flutter都非常大。而且每次创建的话Flutter页面的数据不能保存。因为Flutter要有自己的渲染引擎,不能像iOS的页面一样这样创建,所以建议全局建一个 Flutter 引擎。

     

    4、Flutter 页面调用(二)推荐

    @property (nonatomic, strong) FlutterEngine * flutterEngine; @property (nonatomic, strong) FlutterViewController * flutterVC;

    Flutter 引擎

    - (FlutterEngine *)flutterEngine { if (!_flutterEngine) { //这里不要直接用 _flutterEngine ,然后 _flutterEngine.run 因为在页面将要显示的时候才去执行运行,那么Flutter的页面显示的会非常慢 //_flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"]; //定义一个局部变量,判断一下如果这个flutterEngine已经运行起来了,那么我们的全局_flutterEngine就等于这个 flutterEngine 。失败的话就返回nil FlutterEngine * flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"]; if (flutterEngine.run) {//Flutter 运行运行起来了 _flutterEngine = flutterEngine; } } return _flutterEngine; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil]; self.flutterVC.modalPresentationStyle = UIModalPresentationFullScreen;//模态展示风格(全屏显示) -(void)pushFlutter:(UIButton *)btn { NSString * pageIndex = @"one"; NSString * page = @"one_page"; if (btn.tag == 1002) { pageIndex = @"two"; page = @"two_page"; } [self presentViewController:self.flutterVC animated:YES completion:nil]; }

    4.1、问题

    //    直接这样写就会出错,崩溃. window.defaultRouteName 是空的 为什么是空的呢?因为在run的时候就去运行了Flutter,但是这一个时候并没有去传值。 可以通过单独运行Flutter 代码看一下值,和报错信息

    调试

     

    因为  [vc setInitialRoute:pageIndex]; 传值是在初始化的时候传的,我们现在不每次都去创建Flutter了所以需要改一下。Flutter也不能通过  window.defaultRouteName 来取值了。

    修改Flutter代码

    4.2、Flutter

    import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() => runApp(_MyApp()); // ---------------------------------------- class _MyApp extends StatefulWidget { @override __MyAppState createState() => __MyAppState(); } class __MyAppState extends State<_MyApp> { String pageIndex = 'one'; // 这里是解码器,互相调用 final MethodChannel _oneChannel = MethodChannel('one_page'); final MethodChannel _twoChannel = MethodChannel('two_page'); @override void initState() { // 调用方法一次接收信息 _oneChannel.setMethodCallHandler((call) { setState(() { pageIndex = call.method; }); return null; }); _twoChannel.setMethodCallHandler((call) { setState(() { pageIndex = call.method; }); return null; }); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: rootPage(pageIndex), ); } rootPage(String pageIndex) { switch (pageIndex) { case 'one': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RaisedButton( onPressed: () { // 直接退出页面了,一般不会这么做。 MethodChannel('one_page').invokeMapMethod('exit'); }, child: Text(pageIndex), ), ], )); case 'two': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Center( child: RaisedButton( onPressed: () { MethodChannel('two_page').invokeMapMethod('exit'); }, child: Text(pageIndex), )), ); } } }

     

    5、通讯

    1、FlutterMethodChannel   调用方法(method invocation)一次通讯

    //下面的这里两种都是持续通讯的 2、 FlutterBasicMessageChannel    :传递字符串&半结构化的信息 3、FlutterEventChannel   :用于数据流(stream)的通讯

    完整的代码

    5.1、iOS

    // ViewController.m // NativeDemo // // Created by liujilou on 2020/6/28. // Copyright © 2020 liujilou. All rights reserved. // // FlutterMethodChannel 调用方法(method invocation)一次通讯 //下面的这里两种都是持续通讯的 // FlutterBasicMessageChannel :传递字符串&半结构化的信息 // FlutterEventChannel :用于数据流(stream)的通讯 #import "ViewController.h" #import <Flutter/Flutter.h> @interface ViewController () @property (nonatomic, strong) FlutterEngine * flutterEngine; @property (nonatomic, strong) FlutterViewController * flutterVC; @property (nonatomic, strong) FlutterBasicMessageChannel * msgChannel; @end @implementation ViewController - (FlutterEngine *)flutterEngine { if (!_flutterEngine) { //这里不要直接用 _flutterEngine ,然后 _flutterEngine.run 因为在页面将要显示的时候才去执行运行,那么Flutter的页面显示的会非常慢 //_flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"]; //定义一个局部变量,判断一下如果这个flutterEngine已经运行起来了,那么我们的全局_flutterEngine就等于这个 flutterEngine 。失败的话就返回nil FlutterEngine * flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"]; if (flutterEngine.run) {//Flutter 运行运行起来了 _flutterEngine = flutterEngine; } } return _flutterEngine; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil]; self.flutterVC.modalPresentationStyle = UIModalPresentationFullScreen;//模态展示风格(全屏显示) // 接收Flutter 的数据 // 这里因为messenger 需要 FlutterBinaryMessenger 类型所以报警告 self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVC]; [self.msgChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) { NSLog(@"收到Flutter 的%@",message); }]; CGSize viewSize = self.view.frame.size; UIButton * button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button1.frame = CGRectMake((viewSize.width-100)/2, 100, 100, 40); button1.backgroundColor = [UIColor orangeColor]; button1.tag = 1001; [button1 setTitle:@"按钮一" forState:(UIControlStateNormal)]; [button1 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)]; [self.view addSubview:button1]; UIButton * button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button2.frame = CGRectMake((viewSize.width-100)/2, 200, 100, 40); button2.backgroundColor = [UIColor greenColor]; button2.tag = 1002; [button2 setTitle:@"按钮二" forState:(UIControlStateNormal)]; [button2 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)]; [self.view addSubview:button2]; } -(void)pushFlutter:(UIButton *)btn { NSString * pageIndex = @"one"; NSString * page = @"one_page"; if (btn.tag == 1002) { pageIndex = @"two"; page = @"two_page"; } // 不要每次都去alloc init 一个新的FLutter,这样非常消耗性能 // FlutterViewController * vc = [[FlutterViewController alloc] init]; // 直接这样写就会出错,奔溃. // window.defaultRouteName 是空的 // FlutterViewController * vc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil]; // // vc.modalPresentationStyle = UIModalPresentationFullScreen;//模态跳转页面全屏显示 // 问题就出来 Route,Route在run之后,所以Flutter收不到初始化的数据,这里就不用Route 了。 // 因为Route本身就是在初始化的时候传值用的,我们既然不想让每次都去重新创建,那就不会每次都初始化所以这里用Route 就不合适了 // [vc setInitialRoute:pageIndex];//初始化的时候带过去的值 // [self presentViewController:vc animated:YES completion:nil]; // 使用Channel 通道传值 FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:page binaryMessenger:self.flutterVC]; // 发送消息 [methodChannel invokeMethod:pageIndex arguments:nil]; [self presentViewController:self.flutterVC animated:YES completion:nil]; // 监听 Flutter 回调回来的参数 [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { if ([call.method isEqualToString:@"exit"]) { [self.flutterVC dismissViewControllerAnimated:YES completion:nil]; } }]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { static int a = 0; [self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]]; } @end

    做持续通讯

    iOS点击屏幕,就向Flutter发送消息。将a 值传过去

     

    5.2、Flutter

    import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() => runApp(_MyApp() // MyApp( // window 需要导入import 'dart:ui'; // window.defaultRouteName 拿到的就是 iOS中写的 [vc setInitialRoute:@"one"];带过来的值 one // pageIndex: window.defaultRouteName // ) ); // ---------------------------------------------------- class MyApp extends StatelessWidget { final String pageIndex; const MyApp({Key key, this.pageIndex}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: rootPage(pageIndex), ); } rootPage(String pageIndex) { switch (pageIndex) { case 'one': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Center( child: RaisedButton( onPressed: () { // 直接退出页面了,一般不会这么做。 MethodChannel('one_page').invokeMapMethod('exit'); }, child: Text(pageIndex), ))); case 'two': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Center( child: RaisedButton( onPressed: () { MethodChannel('two_page').invokeMapMethod('exit'); }, child: Text(pageIndex), )), ); } } } // ---------------------------------------- class _MyApp extends StatefulWidget { @override __MyAppState createState() => __MyAppState(); } class __MyAppState extends State<_MyApp> { String pageIndex = 'one'; // 这里是解码器,互相调用 final MethodChannel _oneChannel = MethodChannel('one_page'); final MethodChannel _twoChannel = MethodChannel('two_page'); // 这个是通讯,也需要一个解码器 final BasicMessageChannel _messageChannel = BasicMessageChannel('messageChannel', StandardMessageCodec()); @override void initState() { // 可以持续接收信息 _messageChannel.setMessageHandler((message) { print('收到了来自iOS的:$message'); return null; }); // 调用方法一次接收信息 _oneChannel.setMethodCallHandler((call) { setState(() { pageIndex = call.method; }); return null; }); _twoChannel.setMethodCallHandler((call) { setState(() { pageIndex = call.method; }); return null; }); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: rootPage(pageIndex), ); } rootPage(String pageIndex) { switch (pageIndex) { case 'one': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RaisedButton( onPressed: () { // 直接退出页面了,一般不会这么做。 MethodChannel('one_page').invokeMapMethod('exit'); }, child: Text(pageIndex), ), TextField( // 输入框写数据,向iOS发送数据 onChanged: (String str) { _messageChannel.send(str); }, ) ], )); case 'two': return Scaffold( appBar: AppBar(title: Text(pageIndex)), body: Center( child: RaisedButton( onPressed: () { MethodChannel('two_page').invokeMapMethod('exit'); }, child: Text(pageIndex), )), ); } } }

     

    Flutter 和原生页面不要频繁来回切换,内存消耗会非常大。 同时Flutter 销毁是不会完全销毁的,所以就不要去销毁了,就整体保存一份引擎避免重复创建。

    混合开发 可以利用FLutter 作为项目的主要开发框架

    也可以如上,将FLutter作为一个业务,iOS做框架。但是还是那句话不要频繁的来回切换。

    Processed: 0.010, SQL: 9