iOS 原生项目嵌入 Flutter

虽然一般不建议在原生项目中嵌入 Flutter,但是 Flutter 也可以支持这种方式,下面我们来看一下具体的实现。

原生嵌入 Flutter 的工程配置

如图,我们想使原生嵌入 Flutter 的话,使用 Android Studio 创建项目的时候就要选择 Module 进行创建,使之作为一个模块来开发。

打开我们新建的 flutter_module 工程目录可以看到,与创建的 Flutter App 相比,文件里面仍然有 AndroidiOS 工程文件,但是这里只是为了让我们做调试用的,而且这两个文件都是隐藏文件,不过 AndroidiOS 工程中不建议加入原生代码,而且即使加了,打包的时候也不会被打包进去。flutter_module 是一个纯 Flutter 的工程。

  • Podfile 文件配置
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!

  # Pods for NativeDemo

end

我们使用 Xcode 创建一个原生工程,NativeDemo,使用终端,cdNativeDemo 目录下,pod init,然后配置 Podfile 文件,然后执行 pod install

pod install 完成之后,打开原生项目,引用头文件 #import <Flutter/Flutter.h>,可以成功的话就代表配置成功,现在的话原生工程与 Flutter 就有联系了,下面我们就可以实现代码了,来使原生工程中嵌入 Flutter

原生项目调起 Flutter 页面

  • 原生代码部分
#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 * engine = [[FlutterEngine alloc] initWithName:@"hank"];
        if (engine.run) {
            _flutterEngine = engine;
        }
    }
    return _flutterEngine;
}

- (IBAction)pushFlutter:(id)sender {
    
    self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;

    //创建channel
    FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:self.flutterVc.binaryMessenger];
    //告诉Flutter对应的页面
    [methodChannel invokeMethod:@"one" arguments:nil];
    
    //弹出页面
    [self presentViewController:self.flutterVc animated:YES completion:nil];
    
    //监听退出
    [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        //如果是exit我就退出页面!
        if ([call.method isEqualToString:@"exit"]) {
            [self.flutterVc dismissViewControllerAnimated:YES completion:nil];
        }
    }];
}
- (IBAction)pushFlutterTwo:(id)sender {
    self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;

    //创建channel
    FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:@"two_page" binaryMessenger:self.flutterVc.binaryMessenger];
    //告诉Flutter对应的页面
    [methodChannel invokeMethod:@"two" arguments:nil];
    
    //弹出页面
    [self presentViewController:self.flutterVc animated:YES completion:nil];
    
    //监听退出
    [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        //如果是exit我就退出页面!
        if ([call.method isEqualToString:@"exit"]) {
            [self.flutterVc dismissViewControllerAnimated:YES completion:nil];
        }
    }];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.flutterVc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
    self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVc.binaryMessenger];
    
    [self.msgChannel setMessageHandler:^(id  _Nullable message, FlutterReply  _Nonnull callback) {
        NSLog(@"收到Flutter的:%@",message);
    }];
    
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int a = 0;
    [self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
}

在原生代码部分我们定义了三个属性,flutterEngine 代表引擎对象,flutterVcFlutterViewController 类型的控制器对象,msgChannel 是通信方式中的一种 channel,为 FlutterBasicMessageChannel 类型,下面会有介绍。

在这里我们实现了 pushFlutterpushFlutterTwo 两个方法,代表调起两个不同的 Flutter 页面。在这两个方法中,我们首先创建 methodChannel 对象,并分别传入 onetwo 两个字符串标识,并且 binaryMessenger 传参传入的都是 self.flutterVc.binaryMessenger。在两个方法中分别调用 invokeMethod 方法,向 Flutter 页面发送消息,然后弹出页面,并且实现 setMethodCallHandler 方法,在闭包中判断 call.method isEqualToString:@"exit",进行页面的退出。

  • Flutter 代码部分
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final MethodChannel _oneChannel = const MethodChannel('one_page');
  final MethodChannel _twoChannel = const MethodChannel('two_page');
  final BasicMessageChannel _messageChannel =
      const BasicMessageChannel('messageChannel', StandardMessageCodec());

  String pageIndex = 'one';

  @override
  void initState() {
    super.initState();

    _messageChannel.setMessageHandler((message) {
      print('收到来自iOS的$message');
      return Future(() {});
    });

    _oneChannel.setMethodCallHandler((call) {
      pageIndex = call.method;
      print(call.method);
      setState(() {});
      return Future(() {});
    });
    _twoChannel.setMethodCallHandler((call) {
      pageIndex = call.method;
      print(call.method);
      setState(() {});
      return Future(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: _rootPage(pageIndex),
    );
  }

  //根据pageIndex来返回页面!
  Widget _rootPage(String pageIndex) {
    switch (pageIndex) {
      case 'one':
        return Scaffold(
          appBar: AppBar(
            title: Text(pageIndex),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  _oneChannel.invokeMapMethod('exit');
                },
                child: Text(pageIndex),
              ),
              TextField(
                onChanged: (String str) {
                  _messageChannel.send(str);
                },
              )
            ],
          ),
        );
      case 'two':
        return Scaffold(
          appBar: AppBar(
            title: Text(pageIndex),
          ),
          body: Center(
            child: ElevatedButton(
              onPressed: () {
                _twoChannel.invokeMapMethod('exit');
              },
              child: Text(pageIndex),
            ),
          ),
        );
      default:
        return Scaffold(
          appBar: AppBar(
            title: Text(pageIndex),
          ),
          body: Center(
            child: ElevatedButton(
              onPressed: () {
                const MethodChannel('default_page').invokeMapMethod('exit');
              },
              child: Text(pageIndex),
            ),
          ),
        );
    }
  }
}

Flutter 代码中我们定义了 _oneChannel_twoChannel 这两个变量用了接收原生页面发送的消息,并且向原生页面发送消息。定义了变量 pageIndex 用来标识创建那个页面。

initState 方法中调用 setMethodCallHandler 方法,获取到原生页面传来的数据并赋值给 pageIndex,然后调用 setState 方法。

build 方法中我们调用 _rootPage 方法来判断创建哪个页面。并且分别在这两个页面的点击事件中调用 invokeMapMethod 方法,代表退出页面,原生页面在 setMethodCallHandler 闭包中接收到 exit 数据后就会调用 [self.flutterVc dismissViewControllerAnimated:YES completion:nil],进行页面的退出。

Flutter 与原生的通信

  • MethodChannelFlutterNative 端相互调用,调用后可以返回结果,可以 Native 端主动调用,也可以 Flutter 主动调用,属于双向通信。此方式为最常用的方式, Native 端调用需要在主线程中执行。
  • BasicMessageChannel: 用于使用指定的编解码器对消息进行编码和解码,属于双向通信,可以 Native 端主动调用,也可 Flutter 主动调用。
  • EventChannel:用于数据流(event streams)的通信, Native 端主动发送数据给 Flutter,通常用于状态的监听,比如网络变化、传感器数据等。

Flutter 与原生通信有三种方式,Flutter 为我们提供了三种 Channel,分别是 MethodChannelBasicMessageChannel
EventChannel。但是我们比较常用的就是 MethodChannelBasicMessageChannel 这两种。因为 MethodChannel 前面已经讲过了,所以这里我们介绍一下 BasicMessageChannel 的用法。

BasicMessageChannel 用法

BasicMessageChannel 的用法与 FlutterMethodChannel 类似,在上面的代码示例中,首先在 Flutter 代码中我们也是定义一个 BasicMessageChannel
类型的变量 _messageChannel,在 _messageChannelsetMessageHandler 闭包中接收来自于原生页面发来的消息,调用 _messageChannelsend 方法向原生页面进行通信,在输入框文字变化的时候都会调用 send 方法。在原生代码中也是类似,定义了 msgChannel 属性,setMessageHandler 中的 block 负责接收消息,sendMessage 发送消息,在 touchesBegan 中向 Flutter 传递 a 的累加值。BasicMessageChannelFlutterMethodChannel 最大的区别就是 FlutterMethodChannel 是单次通讯,而 BasicMessageChannel 是持续通讯。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容