Flutter混合開發-iOS

繁星發表於2020-04-18

本文主要針對現有iOS專案想接入flutter,怎麼接入flutter,如何進行專案管理,以及Native和flutter之間如何呼叫,如何除錯來講解的。

一、建立Flutter Module

執行下面的命令建立Flutter Moudle

cd some/path/
flutter create --template module my_flutter
複製程式碼

some/path/是你要存放工程的目錄,然後建立flutter Module,這一步要注意,不要建立成flutter project專案了,執行命令後,控制檯會列印:

Creating project my_flutter... androidx: true
  my_flutter/test/widget_test.dart (created)
  my_flutter/my_flutter.iml (created)
  my_flutter/.gitignore (created)
  my_flutter/.metadata (created)
  my_flutter/pubspec.yaml (created)
  my_flutter/README.md (created)
  my_flutter/lib/main.dart (created)
  my_flutter/my_flutter_android.iml (created)
  my_flutter/.idea/libraries/Flutter_for_Android.xml (created)
  my_flutter/.idea/libraries/Dart_SDK.xml (created)
  my_flutter/.idea/modules.xml (created)
  my_flutter/.idea/workspace.xml (created)
Running "flutter pub get" in my_flutter...                          1.8s
Wrote 12 files.

All done!
Your module code is in my_flutter/lib/main.dart.
複製程式碼

建立完成以後my_flutter檔案結構如下:

my_flutter/
├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

複製程式碼

接下來可以在lib中新增程式碼邏輯,在pubspec.yaml中,新增依賴的packages和plugins。

二、整合方式

1.使用CocoaPods和Flutter SDK整合

這個方案是針對高於Flutter 1.8.4-pre.21版本的SDK的混編方案,如果使用之前的SDK,請檢視Upgrading Flutter added to existing iOS Xcode projectAdd Flutter to existing apps

1.1 Podfile 中新增下面配置

flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
複製程式碼

../my_flutter是你flutter Moudle存放的目錄,這裡my_flutter存放在profile的上一級目錄,所以這麼寫。

1.2 Podfile target 中新增install_all_flutter_pods(flutter_application_path)

target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end
複製程式碼

這裡的MyApp就是對應iOS專案的名稱,存放到自己專案對應target中就好了。

1.3 pod install

在Podfile所在目錄,執行pod install,如果沒問題,會在你的專案中增加以下依賴:

Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing my_flutter (0.0.1)
複製程式碼

在執行pod install以後,如果沒有增加上面?的依賴,那麼可能是工程有問題。

問題1.Profile中路徑新增錯誤或者my_flutter是Flutter project,不是Flutter Moudle 提示錯誤如下:

[!] Invalid `Podfile` file: cannot load such file -- ./my_flutter/.ios/Flutter/podhelper.rb.

 #  from /Users/Example/Podfile:10
 #  -------------------------------------------
 #
 >  load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
 #
 #  -------------------------------------------
複製程式碼

這時候要檢查下flutter_application_path是否正確,如果正確,檢視下my_flutter是否是Flutter project,可以檢視下my_flutter是否包含,.iOS這個檔案,注意這是一個隱藏檔案,先要在電腦中設定成顯示隱藏你檔案,再進行檢視確認,如果包含則是Moudle,不包含則是project。

問題2.專案簽名不對

在my_flutter目錄下執行如下命令:

open -a Simulator
flutter build ios
複製程式碼

檢視能否正確執行,如果提示以下錯誤,則是證書問題:

It appears that your application still contains the default signing identifier.
Try replacing 'com.example' with your signing id in Xcode:
  open ios/Runner.xcworkspace
Encountered error while building for device.
複製程式碼

怎麼解決這個問題?

方法一:

  • 1.找到my_flutter/.ios,開啟Runner.xcworkspace檔案
  • 2.找到Signing & Capabilities,將Signing證書配置正確就可以了(這裡要配置Generic iOS Deveice)

方法二: 使用如下命令,忽略簽名:

flutter build ios --release --no-codesign
複製程式碼

配置成功之後,再次執行flutter build ios,會列印如下資訊:

Automatically signing iOS for device deployment using specified development team in Xcode project:
56XB5ELH9A
Running Xcode build...
 ├─Building Dart code...                                    78.1s
 ├─Generating dSYM file...                                   0.1s
 ├─Stripping debug symbols...                                0.0s
 ├─Assembling Flutter resources...                           0.9s
 └─Compiling, linking and signing...                         2.9s
Xcode build done.                                           84.0s
Built
複製程式碼

然後再次pod install應該就可以成功了。

1.4 方案優缺點

優點:

  • 1.功能配置簡單,方便管理。
  • 2.使用CocoaPods便於整合。

缺點:

  • 1.團隊成員都必須配置flutter環境,否則編譯不過
  • 2.Native程式碼和Flutter程式碼存放在一起,會變得複雜。

2.framework方式接入

2.1生成FrameWork

首先切換到my_flutter所在目錄,執行下列命令,生成framework

flutter build ios-framework --output=../Flutter/
複製程式碼

命令執行成功後,會在my_flutter同一級目錄下,產生Flutter的檔案,檔案結構如下:

Flutter/
├── Debug/
│   ├── Flutter.framework
│   ├── App.framework
│   ├── FlutterPluginRegistrant.framework (only if you have plugins with iOS platform code)
│   └── example_plugin.framework (each plugin is a separate framework)
├── Profile/
│   ├── Flutter.framework
│   ├── App.framework
│   ├── FlutterPluginRegistrant.framework
│   └── example_plugin.framework
└── Release/
    ├── Flutter.framework
    ├── App.framework
    ├── FlutterPluginRegistrant.framework
    └── example_plugin.framework
複製程式碼

2.2配置frameWork路徑

在專案中找到這個路徑build settings > Build Phases > Link Binary With Libraries 新增$(PROJECT_DIR)/Flutter/Release/Framework Search Paths

配置

2.3嵌入frameWork

在專案中找到這個路徑General > Frameworks,Libraries and Embedded Content app.FrameworkFlutter.FrameWork新增到專案中,就可以使用了。

問題1.Failed to find assets path for "flutter_assets"

Failed to find assets path for "flutter_assets"
[VERBOSE-2:engine.cc(114)] Engine run configuration was invalid.
複製程式碼

如果報上面的錯誤,則在my_flutter中執行以下命令:

flutter clean
flutter build ios
複製程式碼

問題2.dyld: Library not loaded: @rpath/Flutter.framework/Flutter

這個問題是說明嵌入frameWork有問題,可以檢查一下,Embed Framework和Link Binary With Libraries

Embed Framework

2.4 方案優缺點

優點:

  • 1.團隊成員不依賴flutter環境

缺點:

  • 1.打包配置,比較麻煩,都需要手動操作。

3.使用Flutter framework和CocoaPods整合(本地)

3.1生成frameWork

在Flutter v1.13.6之後版本,支援--cocoapods引數,可以使用下面命令。

flutter build ios-framework --cocoapods --output=../Flutter/
複製程式碼

生成如下檔案結構:

Flutter/
├── Debug/
│   ├── Flutter.podspec
│   ├── App.framework
│   ├── FlutterPluginRegistrant.framework
│   └── example_plugin.framework (each plugin with iOS platform code is a separate framework)
├── Profile/
│   ├── Flutter.podspec
│   ├── App.framework
│   ├── FlutterPluginRegistrant.framework
│   └── example_plugin.framework
└── Release/
    ├── Flutter.podspec
    ├── App.framework
    ├── FlutterPluginRegistrant.framework
    └── example_plugin.framework
複製程式碼

3.2配置profile檔案

pod 'Flutter', :podspec => '../Flutter/{build_mode}/Flutter.podspec'

複製程式碼

3.3 方案優缺點

優點:

  • 1.團隊成員不依賴flutter環境
  • 2.可以使用cocoapods整合管理。

缺點:

  • 1.Flutter版本有限制
  • 2.每次需要自己打frmaework

4.使用Flutter framework和CocoaPods整合(遠端)

4.1建立一個CocoaPods私有庫

在my_flutter同級目錄下,建立CocoaPods私有庫

$ pod lib create MyFlutterFramework
複製程式碼

終端執行程式碼:

 xingkunkun:FlutterForFW admin$ pod lib create MyFlutterFramework
 Cloning `https://github.com/CocoaPods/pod-template.git` into `MyFlutterFramework `.
Configuring MyFlutter template.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.

What platform do you want to use?? [ iOS / macOS ]
 > ios
What language do you want to use?? [ Swift / ObjC ]
 > objc
Would you like to include a demo application with your library? [ Yes / No ]
 > no
Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > none
Would you like to do view based testing? [ Yes / No ]
 > no
What is your class prefix?
 >

Running pod install on your new library.
複製程式碼

4.2建立一個Flutter Module

  • 1.建立Flutter Module步驟,
flutter create --template module my_flutter
複製程式碼
  • 2.構建framework
$ flutter build ios --debug 
 或者 
flutter build ios --release --no-codesign(選擇不需要證書)
複製程式碼
  • 3.檢查.ios目錄下
    • 是否有Flutter-->App.framework
    • 是否有Flutter-->engine-->Flutter.framework
.ios目錄下
Flutter-->App.framework
Flutter-->engine-->Flutter.framework
複製程式碼

4.3將CocoaPods私有庫整合到Native專案中

在MyFlutterFramework中建立ios_frameworks資料夾,並將App.frameworkFlutter.framework拷貝進去。

在MyFlutterFramework的podspec檔案中,新增以下配置:

  s.static_framework = true
  arr = Array.new
  arr.push('ios_frameworks/*.framework')
  s.ios.vendored_frameworks = arr
複製程式碼

之後在MyFlutterFramework的podfile同級目錄中執行

$ pod install
複製程式碼

在MyApp工程下的podfile檔案中新增

platform :ios, '8.0'

target 'MyApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for MyApp
   pod 'MyFlutterFramework', :path => '../MyFlutterFramework'

end
複製程式碼

之後在MyApp的podfile同級目錄中執行

$ pod install
複製程式碼

這時在MyApp中,就可以找到App.frameworkFlutter.framework

4.4將MyFlutterFramework和my_flutter推送到遠端倉庫

  • 1.MyFlutterFramework和my_flutter推送到遠端倉庫
  • 2.修改MyApp工程下的podfile,將pod 'MyFlutterFramework'依賴修改為MyFlutterFramework遠端連線。
platform :ios, '8.0'

target 'MyApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for MyApp
   pod 'MyFlutterFramework', :git=>'https://gitlab.com/MyFlutterFramework.git'

end
複製程式碼
    1. 如果MyFlutterFramework中的ios_frameworks不詳推送到遠端倉庫,可以在gitignore檔案中新增一下
# 忽略ios_frameworks中檔案
ios_frameworks

複製程式碼

4.5 方案優缺點

優點:

  • 1.團隊成員不依賴flutter環境
  • 2.可以使用cocoapods整合管理。
  • 3.可以使用遠端倉庫共享和管理專案程式碼

缺點:

  • 1.每次重新構建,需要移動framework位置,比較繁瑣,可以使用指令碼解決。

三、Flutter與Native互動

Flutter 官方提供了一種 Platform Channel 的方案,用於 Dart 和平臺之間相互通訊。

核心原理:

  • Flutter應用通過Platform Channel將傳遞的資料編碼成訊息的形式,跨執行緒傳送到該應用所在的宿主(Android或iOS);
  • 宿主接收到Platform Channel的訊息後,呼叫相應平臺的API,也就是原生程式語言來執行相應方法;
  • 執行完成後將結果資料通過同樣方式原路返回給應用程式的Flutter部分。

Flutter提供了三種不同的Channel:

  • BasicMessageChannel(主要是傳遞字串和一些半結構體的資料)
  • MethodChannel(用於傳遞方法呼叫)
  • EventChannel(資料流的通訊)

下面是使用Platform Channel進行通訊的示例:

1.Native app主動與Flutter互動

Native app主動與Flutter,Native使用FlutterEventChannel來監聽訊息,並通過FlutterEventSink來傳送訊息,Flutter使用MethodChannel監聽訊息,互動主要分為三步:

  • 1.flutter註冊EventChannel
  • 2.flutter EventChannel監聽native訊息
  • 3.native通過EventChannel傳送訊息

Flutter程式碼


class HomePage extends StatefulWidget {
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // 註冊一個通知
  static const MethodChannel methodChannel = const MethodChannel('com.pages.your/native_get');

  // 渲染前的操作,類似viewDidLoad
  @override
  void initState() {
    super.initState();

    // 監聽事件,同時傳送引數12345
    eventChannel.receiveBroadcastStream(12345).listen(_onEvent,onError: _onError);
  }

  String naviTitle = 'NativeToFlutter' ;
  // 回撥事件
  void _onEvent(Object event) {
    setState(() {
      naviTitle =  event.toString();
    });
  }
  // 錯誤返回
  void _onError(Object error) {

  }

  _iOSPushToVC() async {
    await methodChannel.invokeMethod('FlutterToNative');
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Material(
        child: new Scaffold(
          body: new Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new Padding(
                  padding: EdgeInsets.only(top: 20),
                  child: new Text(naviTitle),
                ),
              ],
            )
          ),
        ),
      ),
    );
  }
}


複製程式碼

Native程式碼

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

@interface ViewController ()<FlutterStreamHandler>

// 定義FlutterEventSink的快取物件
@property (nonatomic, strong) FlutterEventSink eventSink;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self action:@selector(handleButtonAction) forControlEvents:UIControlEventTouchUpInside];
    
    [button setTitle:@"載入Flutter" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    button.frame = CGRectMake(100, 100, 160, 60);
    [self.view addSubview:button];

}

- (void)handleButtonAction
{
    FlutterViewController* flutterViewController = (FlutterViewController*)self.window.rootViewController;
    // 設定路由名字 跳轉到不同的flutter介面
    FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"com.pages.your/native_post" binaryMessenger:flutterViewController];
    [eventChannel setStreamHandler:self];
}


#pragma mark - <FlutterStreamHandler>
// // 這個onListen是Flutter端開始監聽這個channel時的回撥,第二個引數 EventSink是用來傳資料的載體。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events {
    
    // arguments flutter給native的引數
    // 回撥給flutter, 建議使用例項指向,因為該block可以使用多次
    self.eventSink = events;
    events(@"啦啦啦");
    return nil;
}

/// flutter不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
    // arguments flutter給native的引數
    return nil;
}

@end

複製程式碼

2.Flutter主動與Native app互動

Flutter主動與Native app互動,Native使用FlutterMethodChannel來監聽訊息,並接聽訊息,Flutter使用EventChannel來傳送訊息,互動主要分為以下幾步:

  • 1.Native建立MethodChannel。
  • 2.Native新增HandleBlcok。
  • 3.Flutter傳送訊息。

Flutter程式碼

class HomePage extends StatefulWidget {
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  // 註冊一個通知
  static const EventChannel eventChannel = const EventChannel('com.pages.your/native_post');

  _iOSPushToVC() async {
    await methodChannel.invokeMethod('FlutterToNative');
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Material(
        child: new Scaffold(
          body: new Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new GestureDetector(
                  behavior: HitTestBehavior.opaque,
                  child: Container(
                    color: Colors.red,
                    child: new Text('Flutter to Native'),
                  ),
                  onTap: (){
                    _iOSPushToVC();
                  },
                ),
              ],
            )
          ),
        ),
      ),
    );
  }
}
複製程式碼

Native程式碼

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self action:@selector(handleButtonAction) forControlEvents:UIControlEventTouchUpInside];
    
    [button setTitle:@"載入Flutter" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    button.frame = CGRectMake(100, 100, 160, 60);
    [self.view addSubview:button];

}

- (void)handleButtonAction
{

    FlutterViewController* flutterViewController = (FlutterViewController*)self.window.rootViewController;
    
    // 要與main.dart中一致
    NSString *channelName = @"com.pages.your/native_get";
    FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterViewController];

    [messageChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
      // call.method 獲取 flutter 給回到的方法名,要匹配到 channelName 對應的多個 傳送方法名,一般需要判斷區分
      // call.arguments 獲取到 flutter 給到的引數,(比如跳轉到另一個頁面所需要引數)
      // result 是給flutter的回撥, 該回撥只能使用一次
      NSLog(@"method=%@ \narguments = %@", call.method, call.arguments);
      
      // method和WKWebView裡面JS互動很像
      // flutter點選事件執行後在iOS跳轉TargetViewController
      if ([call.method isEqualToString:@"FlutterToNative"]) {
			NSLog(@"method=%@ \narguments = %@", call.method, call.arguments);
      }
    }];

  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end
複製程式碼

四、app除錯

混合開發的時候,需要在XCode除錯程式碼,在除錯的過程中,怎麼除錯Dart程式碼呢?或者能不能使用熱載入?

1.除錯Dart程式碼

在混合開發過程中,在iOS專案中,我們如何除錯dart程式碼呢?

  • 1.關閉我們的app
  • 2.點選Android Studio工具欄上的Flutter Attach按鈕
    • 點選之後會提示Waiting for a connection from Flutter on iPhone 11 Pro...
  • 3.啟動我們的app
    • 啟動app之後,會提示Syncing files to device iPhone 11 Pro...

接下來就可以像除錯普通Flutter專案一樣來除錯混合開發模式下的Dart程式碼了。

2.熱載入

  • 1.關閉我們的app
  • 2.在terminal中執行 flutter attach命令。
$ flutter attach
Waiting for a connection from Flutter on iPhone 11 Pro Max...
複製程式碼

注意,這裡如果提示有多個裝置,如下所示:

More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

我的 iPhone       • 00008030-000445611146802E            • ios • iOS 13.3
iPhone 11 Pro Max • 67FCC5B2-DA5D-4EF0-8DE1-53E8F8C4CBA9 • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-2 (simulator)

複製程式碼

可以使用以下命令:

 flutter attach -d 67FCC5B2-DA5D-4EF0-8DE1-53E8F8C4CBA9 //67FCC5B2-DA5D-4EF0-8DE1-53E8F8C4CBA9是裝置對應的id
複製程式碼
  • 3.啟動app,啟動之後會有如下提示,就代表成功了。
Syncing files to device iPhone 11 Pro Max...                            
 4,196ms (!)                                       

?  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone 11 Pro Max is available at: http://127.0.0.1:62889/T9DjblAu03w=/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".
複製程式碼

接下來就可以在terminal除錯了:

r : 熱載入;
R : 熱重啟;
h : 獲取幫助;
d : 斷開連線;
q : 退出;
複製程式碼

參考資料:
Integrate a Flutter module into your iOS project
Upgrading Flutter added to existing iOS Xcode project
Add Flutter to existing apps
flutter整合進iOS工程
閒魚flutter-boot介紹
優雅的 Flutter 元件化 混編方案
Flutter和原生iOS互動
Flutter混合開發(二):iOS專案整合Flutter模組詳細指南
深入理解Flutter Platform Channel
Flutter混合開發二-FlutterBoost使用介紹
如何用 Flutter 實現混合開發?
深入理解Flutter的Platform Channel機制

相關文章