Flutter 與Native原生互動

尤先森發表於2019-09-10

學習Flutter也有一段時間了,今天來介紹一下Flutter是如何與原生互動的。

原生互動的重要性就不用說了吧。畢竟Flutter也不是萬能的,有時候還是需要我們們原生的支援,才能達成各種奇奇怪怪的需求,那麼話不多說,直接開幹。

1. 新建一個Flutter工程

這次我們的目的是與原生互動,那麼建立方式自然與先前不同 之前選擇的是 Flutter Application 普通工程 這次我們選擇 Flutter Module 互動工程

Flutter 與Native原生互動

Flutter 與Native原生互動

從上圖可以看出,剛建立出來的工程,與普通的Flutter Application不同 androidios資料夾的名稱前面都多了個.,在本地資料夾中檢視帶 .的資料夾可以發現這兩個資料夾是隱藏資料夾。

Flutter 與Native原生互動

那麼為什麼要把這兩個資料夾隱藏起來呢? 答:這兩個資料夾的內容與普通的Flutter Application一樣,但是這兩個工程只是用來給測試的,不參與到原生互動當中。 所謂的測試就是我們在Android Studio中Run一個專案。

2. Xcode新建一個Native工程

建立一個Xcode專案與Flutter專案同一路徑,如下圖

Flutter 與Native原生互動

3. 新增依賴

3.1 cocoapods 引入Flutter支援
  1. $cd Native工程路徑
  2. $pod init
  3. 編輯 podFile 內容 (內容在下面,注意看中文內容)
  4. pod install
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'FlutterNative' do
  flutter_application_path = '../你的flutter資料夾/'
  eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

end
複製程式碼
3.2 編譯Native內容
  • 開啟Native資料夾中的.xcworkspace
  • 關閉Bitcode
  • 編譯一下工程,一般情況都會Build Success
3.3 設定Flutter編譯指令碼
  • TARGET -> Build Phases -> 新增指令碼
    image.png
  • 輸入指令碼內容***(內容如下)***
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
複製程式碼

指令碼內容可以在對應路徑下找到,有興趣的同學可以自行翻閱。

  • 移動指令碼編譯位置 因為Build Success是有編譯順序的,為了避免一些不必要的情況。 按下圖操作。

    image.png

  • command + B 跑一下

3.4 寫程式碼
  1. 我在Main.storyboard中建立了個按鈕,並將其點選事件拖到 ViewController.m 中。
  2. 宣告瞭一個FlutterViewController屬性變數
  3. 在點選事件中,present這個VC

tips: FlutterViewController無需重複建立,一旦載入之後,在程式執行期間將永!不!釋!放!,重複建立將會導致記憶體佔用越來越大。

#import "ViewController.h"
#import <Flutter/Flutter.h>

@interface ViewController ()

@property(strong,nonatomic)FlutterViewController *flutterVC;

@end

@implementation ViewController
- (IBAction)FirstBtnClick:(id)sender {
    
    self.flutterVC = [FlutterViewController new];
    [self presentViewController:self.flutterVC animated:YES completion:nil];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
複製程式碼

#####3.5 執行程式(Xcode工程)

  • 看到我們建立好的Flutter工程最初的樣子

    新Fluuter工程.png

  • Flutter中嘗試修改一下標題,重新執行Xcode工程

    修改標題.png

至此,我們就完成了Flutter與Native原生互動的第一步!搭建好Flutter 與Native 原生之間的一道橋。

###4. 開始互動 我們開始在上文程式碼的基礎上,繼續編寫互動程式碼。 #####一、設定預設初始化路由頁面 1. Xcode工程設定初始化路由

- (IBAction)FirstBtnClick:(id)sender {
    self.flutterVC = [FlutterViewController new];
    //設定初始化路由
    [_flutterVC setInitialRoute:@"pageID"];
    [self presentViewController:self.flutterVC animated:YES completion:nil];
}
複製程式碼

2. Flutter工程中設定預設路由名稱

2.1 import 'dart:ui'; 2.2. 宣告一個變數,接收Native傳送過來的字串 final String pageIdentifier; 2.3 Flutter列印接收到的內容

import 'package:flutter/material.dart';
import 'dart:ui';
//傳入window.defaultRouteName
void main() => runApp(MyApp(pageIdentifier: window.defaultRouteName,));


class MyApp extends StatefulWidget {
  //宣告接收變數
  final String pageIdentifier;

  const MyApp({Key key, this.pageIdentifier}) : super(key: key);
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    //列印widget.pageIdentifier
    print(widget.pageIdentifier);
    return MaterialApp(
      home: Container(
        color: Colors.white,
        child: Center(
          child: Text(widget.pageIdentifier),
        ) ,
      )
    );
  }
}
複製程式碼

效果如下:

image.png

3. 配置不同ID做不同的事 在Native端配置不同的setInitialRoute,然後在Flutter端接收到之後,根據不同的ID,顯示不同的頁面。 示例程式碼如下:

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    print(widget.pageIdentifier);

    switch(widget.pageIdentifier){
      case 'pageA':{
        return PageA();
      }
      case 'pageB':{
        return PageB();
      }
      case 'pageC':{
        return PageC();
      }
      defult:{
        return DefalutPage();
      }
    }
  }
}
複製程式碼

警告:預設初始路由只能設定一次!後面反覆設定時,不論傳遞的是什麼,預設路由都是第一次進入時的那個。 原因是因為我們Flutter中,是用一個final修飾符修飾的變數接收,所以如果想換初始路由,我們需要重新建立一個FlutterViewController,但是這又是耗費記憶體。 所以,如果要跳轉不同介面,還請繼續往下看


#####二、 Flutter傳遞資料給Native

這裡我們建立在上文內容的基礎上,給PageID加上一個點選事件,在點選PageID之後,退出Flutter介面,回到Native介面。

1. 引入服務

import 'package:flutter/services.dart';

2. MethodChannel

2.1 Flutter點選事件使用MethodChannel傳遞資料

child: GestureDetector(
    onTap: () {
      MethodChannel('test').invokeListMethod('dismiss','這裡寫引數');
    },
    child: Text(widget.pageIdentifier),
),
複製程式碼

2.2 Native建立FlutterMethodChannel並設定回撥

    self.flutterVC = [FlutterViewController new];
    
    FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"test" binaryMessenger:self.flutterVC];
    
    [channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        NSLog(@"%@ -- %@",call.method,call.arguments);
        if ([call.method isEqualToString:@"dismiss"]) {
            [self.flutterVC dismissViewControllerAnimated:YES completion:nil];
        }
    }];
複製程式碼

2.3 測試通道是否連通

image.png

如上,我們已經成功獲取了Flutter端傳遞給Native端的資料。 接下來,我們再試試Native如何通過MethodChannel傳遞引數給Flutter。

#####三、Native傳遞資料給Flutter

這裡我們需要修改的內容比較多,請耐心看。

  1. Flutter建立MethodChannel
  final MethodChannel _channerOne = MethodChannel('pageOne');
  final MethodChannel _channerTwo = MethodChannel('pageTwo');
  final MethodChannel _channerDefault = MethodChannel('pageDefault');
複製程式碼
  1. 設定變數獲取介面名稱以及初始化判斷
  var _pageName = '';
  var _initialized = false;
複製程式碼
  1. 通道設定通道回撥
   @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _channerOne.setMethodCallHandler((MethodCall call){
      print('這是One接收原生的回撥${call.method}==${call.arguments}');
       _pageName = call.method;
       setState(() {});
    });
    _channerTwo.setMethodCallHandler((MethodCall call){
      print('這是Two接收原生的回撥${call.method}==${call.arguments}');
      _pageName = call.method;
      setState(() {});
    });
    _channerDefault.setMethodCallHandler((MethodCall call){
      print('這是Default接收原生的回撥${call.method}==${call.arguments}');
      _pageName = call.method;
      setState(() {});
    });
  }
複製程式碼

4.設定build方法

  @override
  Widget build(BuildContext context) {
    
    //如果還沒初始化那麼判斷規則就用初始化時傳進來的ID
    String switchValue = _initialized?_pageName:widget.pageIdentifier;
    //標記已經初始化
    _initialized = true;
    
    switch(switchValue){
      case 'pageOne':{
        return MaterialApp(
            home: Container(
              color: Colors.white,
              child: Center(
                child: GestureDetector(
                  onTap: () {
                    _channerOne.invokeListMethod('dismissOne','這是通道一傳回來的資料');
                  },
                  child: Text('這是第一頁'),
                ),
              ) ,
            )
        );
      }
      case 'pageTwo':{
        return MaterialApp(
            home: Container(
              color: Colors.white,
              child: Center(
                child: GestureDetector(
                  onTap: () {
                    _channerTwo.invokeListMethod('dismissTwo','這是通道二傳回來的資料');
                  },
                  child: Text('這是第二頁'),
                ),
              ) ,
            )
        );
      }
      default:{
        return MaterialApp(
            home: Container(
              color: Colors.white,
              child: Center(
                child: GestureDetector(
                  onTap: () {
                    _channerDefault.invokeListMethod('dismissDefault','這是預設通道傳回來的資料');
                  },
                  child: Text('這是預設'),
                ),
              ) ,
            )
        );
      }
    }
  }
複製程式碼
  1. 回到Native端 請注意看註釋
@interface ViewController ()
{
    //標記是否已經初始化過
    BOOL isSettedInitialRoute;
}
@property(strong,nonatomic)FlutterViewController *flutterVC;

@property(strong,nonatomic)FlutterMethodChannel *channelOne;

@property(strong,nonatomic)FlutterMethodChannel *channelTwo;

@property(strong,nonatomic)FlutterMethodChannel *channelDefault;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //建立FlutterViewController(還沒初始化,只有進入到Flutter頁面後才算初始化完成)
    self.flutterVC = [FlutterViewController new];
    //初始化通道一
    self.channelOne = [FlutterMethodChannel methodChannelWithName:@"pageOne" binaryMessenger:self.flutterVC];
    __weak typeof(self) weakself = self;
    //註冊通道一回撥
    [_channelOne setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        NSLog(@"method:%@ -- arguments:%@",call.method,call.arguments);
        if ([call.method isEqualToString:@"dismissOne"]) {
            [weakself.flutterVC dismissViewControllerAnimated:YES completion:nil];
        }
    }];
    //初始化通道二
    self.channelTwo = [FlutterMethodChannel methodChannelWithName:@"pageTwo" binaryMessenger:self.flutterVC];
    //註冊通道二回撥
    [_channelTwo setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        NSLog(@"method:%@ -- arguments:%@",call.method,call.arguments);
        if ([call.method isEqualToString:@"dismissTwo"]) {
            [weakself.flutterVC dismissViewControllerAnimated:YES completion:nil];
        }
    }];
    //初始化預設通道
    self.channelDefault = [FlutterMethodChannel methodChannelWithName:@"pageDefault" binaryMessenger:self.flutterVC];
    //註冊預設通道回撥
    [_channelDefault setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        NSLog(@"method:%@ -- arguments:%@",call.method,call.arguments);
        if ([call.method isEqualToString:@"dismissDefault"]) {
            [weakself.flutterVC dismissViewControllerAnimated:YES completion:nil];
        }
    }];
}

- (IBAction)FirstBtnClick:(id)sender {
    //如果還沒初始化
    if (!isSettedInitialRoute) {
        //設定初始介面
        [self.flutterVC setInitialRoute: @"pageOne"];
        //標記已經初始化
        isSettedInitialRoute = YES;
    }else{
        //如果已經初始化過了,就直接呼叫Flutter中註冊的方法,
        [self.channelOne invokeMethod:@"pageOne" arguments:@"iOS通過通道一發訊息給Flutter"];
    }
    [self presentViewController:self.flutterVC animated:YES completion:nil];
}
- (IBAction)SecondBtnClick:(id)sender {
    //這裡就跟FirstBtnClick同理了
    if (!isSettedInitialRoute) {
        [self.flutterVC setInitialRoute: @"pageTwo"];
        isSettedInitialRoute = YES;
    }else{
        [self.channelTwo invokeMethod:@"pageTwo" arguments:@"iOS通過通道二發訊息給Flutter"];
    }
    [self presentViewController:self.flutterVC animated:YES completion:nil];
}
複製程式碼
  1. 互動效果
    image.png

上文我們利用MethodChannel在Flutter 與 Native 原生之間互動的內容,接下來我們繼續瞭解一下另外一種Channel。

#####BasicMessageChannel 從字面意思呢,我們可以理解為這是一條為傳送基礎訊息資料的通道。 他與MethodChannel有一些區別的地方。接下來就簡單介紹一下這個BasicMessageChannel

1. Flutter端 1.1 建立通道 這裡眼尖的同學會發現,這條通道比MethodChannel多了一個codec引數,這個引數可以理解成"解碼器"。這裡我們用StandardMessageCodec()

final BasicMessageChannel _basicMessageChannel = BasicMessageChannel('basic', StandardMessageCodec());
複製程式碼

1.2 註冊方法回撥

@override
  void initState() {
    // TODO: implement initState
    super.initState();

    _basicMessageChannel.setMessageHandler((message){
      print('收到Xcode發來的訊息 == $message');
    });
  }
複製程式碼

1.3 呼叫訊息通道給Native傳送訊息 這裡由於程式碼比較長,省略無關緊要的部分

Container(
    height: 80,
    color: Colors.red,
    child: TextField(
        onChanged: (value){
            // 將TextField文字內容傳送給Native
            _basicMessageChannel.send(value);
        },
    ),
),
複製程式碼

2. Native端 2.1 宣告屬性 @property(strong,nonatomic)FlutterBasicMessageChannel *basicChannel; 2.2 初始化訊息通道

    self.basicChannel = [FlutterBasicMessageChannel messageChannelWithName:@"basic" binaryMessenger:self.flutterVC];
複製程式碼

2.3 註冊方法回撥

    [_basicChannel setMessageHandler:^(id  _Nullable message, FlutterReply  _Nonnull callback) {
        NSLog(@"basicMessage == %@",message);
    }];
複製程式碼

2.4 Native給Flutter通過訊息通道發訊息

- (IBAction)BasicClick:(id)sender {
    [self.basicChannel sendMessage:@"發一條訊息給fultter"];
    [self presentViewController:self.flutterVC animated:YES completion:nil];
    
}
複製程式碼

3. 互動結果

image.png

###結語 那麼以上就是本次 Flutter 與 Native 原生互動的全部內容。當然 這裡還有另外一種互動方式,EventChannel傳遞資料流,下次有機會的話,在給大家補齊還請見諒。

相關文章