iOS 原生項目嵌入 Flutter

雖然一般不建議在原生項目中嵌入Flutter,但是Flutter也可以支持這種方式,下面我們來看一下具體的實現。

原生嵌入 Flutter 的工程配置

如圖,我們想使原生嵌入Flutter的話,使用Android Studio創建項目的時候就要選擇Module進行創建,使之作爲一個模塊來開發。

打開我們新建的flutter_module工程目錄可以看到,與創建的Flutter App相比,文件裏面仍然有AndroidiOS工程文件,但是這裏只是爲了讓我們做調試用的,而且這兩個文件都是隱藏文件,不過AndroidiOS工程中不建議加入原生代碼,而且即使加了,打包的時候也不會被打包進去。flutter_module是一個純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!

  # 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",進行頁面的退出。

// 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,分別是MethodChannelBasicMessageChannelEventChannel。但是我們比較常用的就是MethodChannelBasicMessageChannel這兩種。因爲MethodChannel前面已經講過了,所以這裏我們介紹一下BasicMessageChannel的用法。

BasicMessageChannel 用法

BasicMessageChannel的用法與FlutterMethodChannel類似,在上面的代碼示例中,首先在Flutter代碼中我們也是定義一個BasicMessageChannel類型的變量_messageChannel,在_messageChannelsetMessageHandler閉包中接收來自於原生頁面發來的消息,調用_messageChannelsend方法向原生頁面進行通信,在輸入框文字變化的時候都會調用send方法。在原生代碼中也是類似,定義了msgChannel屬性,setMessageHandler中的block負責接收消息,sendMessage發送消息,在touchesBegan中向Flutter傳遞a的累加值。

作者:晨曦_iOS

https://juejin.cn/post/7036393182053007367

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kvQC7hrHUgAtK9RjqyFOFg