Flutter 開發(fā)之 Native 集成 Flutter 混合開發(fā)

本文先介紹一下現(xiàn)有工程如何集成 Flutter 實(shí)現(xiàn)混合開發(fā),以及混合項(xiàng)目如何打包,再探索下如何降低原生和 Flutter 之間的依賴,使 Flutter 開發(fā)對(duì)原生開發(fā)的影響盡量降低,以及一些我在嘗試中遇到的問題及解決。

介紹 Flutter

Flutter 是 Google 發(fā)布的一個(gè)用于創(chuàng)建跨平臺(tái)、高性能移動(dòng)應(yīng)用的框架。Flutter 和 QT mobile 一樣,都沒有使用原生控件,相反都實(shí)現(xiàn)了一個(gè)自繪引擎,使用自身的布局、繪制系統(tǒng)。開發(fā)者可以通過 Dart 語言開發(fā) App,一套代碼同時(shí)運(yùn)行在 iOS 和 Android平臺(tái)。Flutter 提供了豐富的組件、接口,開發(fā)者可以很快地為 Flutter 添加 Native 擴(kuò)展。

前提工作

開發(fā)者需要安裝好 Flutter 的環(huán)境,執(zhí)行flutter doctor -v驗(yàn)證。

flutter_doctor_v

驗(yàn)證通過后即可開始集成 Flutter。

現(xiàn)有原生工程集成 Flutter

最官方的教程應(yīng)該是Add Flutter to existing apps了,按照教程如下一步步操作:

1.創(chuàng)建 flutter module

使用flutter create xxx指令創(chuàng)建的 Flutter 項(xiàng)目包括用于 Flutter/Dart 代碼的非常簡單的工程。你可以修改 main.dart 的內(nèi)容,以滿足你的需要,并在此基礎(chǔ)上進(jìn)行構(gòu)建。

假設(shè)你有一個(gè)已經(jīng)存在 iOS 工程(以 flutterHybridDemo 為例)在some/path/flutterHybridDemo,那么你新建的 flutter_module 和 iOS 工程應(yīng)該在同一目錄下(即都在 path 下)。

$ cd some/path/
$ flutter create -t module flutter_module
flutter_module目錄結(jié)構(gòu)

通過shift+command+.顯示/隱藏隱藏文件夾

  • lib/main.dart:存放的是 Dart 語言編寫的代碼,這里是核心代碼;
  • pubspec.yaml:配置依賴項(xiàng)的文件,比如配置遠(yuǎn)程 pub 倉庫的依賴庫,或者指定本地資源(圖片、字體、音頻、視頻等);
  • .ios/:iOS 部分代碼;
  • .android/:Android 部分代碼;
  • build/:存儲(chǔ) iOS 和 Android 構(gòu)建文件;
  • test/:測試代碼。

2.將 flutter module 作為依賴添加到工程

假設(shè)文件夾結(jié)構(gòu)如下:

some/path/
  flutter_module/
    lib/main.dart
    .ios/
    ...
  flutterHybridDemo/
    flutterHybridDemo.xcodeproj
    flutterHybridDemo/
        AppDelegate.h
        AppDelegate.m
        ...

集成 Flutter 框架需要使用CocoaPods,這是因?yàn)?Flutter 框架還需要對(duì) flutter_module 中可能包含的任何 Flutter 插件可用。

- 如果需要,請(qǐng)參考cocoapods.org了解如何在您的電腦上安裝 CocoaPods。

創(chuàng)建 Podfile:

$ cd some/path/flutterHybridDemo
$ pod init

此時(shí)工程中會(huì)出現(xiàn)一個(gè) Podfile 文件,添加項(xiàng)目依賴的第三方庫就在這個(gè)文件中配置,編輯 Podfile 文件添加最后兩行代碼:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'TestOne' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for TestOne

  target 'TestOneTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'TestOneUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

#新添加的代碼
flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
- 如果你的工程(flutterHybridDemo)已經(jīng)在使用 Cocoapods ,你只需要做以下幾件事來整合你的 flutter_module 應(yīng)用程序:

(1)添加如下內(nèi)容到 Podfile:

flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

(2)執(zhí)行pod install

當(dāng)你在some/path/flutter_module/pubspec.yaml中修改 Flutter 插件依賴時(shí),需要先執(zhí)行flutter packages get通過 podhelper.rb 腳本來刷新插件列表,然后再從some/path/flutterHybridDemo執(zhí)行一次pod install。

podhelper.rb 腳本將確保你的插件和 Flutter 框架被添加到你的工程中,以及 bitcode 被禁用。

(3)禁用 bitcode

因?yàn)?Flutter 現(xiàn)在不支持 bitcode。需要設(shè)置 Build Settings->Build Options->Enable Bitcode 為 NO。


bitcode 禁用

3.為編譯 Dart 代碼配置 build phase

打開 iOS 工程,選中項(xiàng)目的 Build Phases 選項(xiàng),點(diǎn)擊左上角+號(hào)按鈕,選擇 New Run Script Phase。


配置 build phase

將下面的 shell 腳本添加到輸入框中:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

最后,確保 Run Script 這一行在 "Target dependencies" 或者 "Check Pods Manifest.lock" 后面。


配置 build phase

至此,你可以編譯一下工程確保無誤:?B。

4.在 iOS 工程中使用 FlutterViewController

首先聲明你的 AppDelegate 是 FlutterAppDelegate 的子類。然后定義一個(gè) FlutterEngine 屬性,它可以幫助你注冊(cè)一個(gè)沒有 FlutterViewController 實(shí)例的插件。

在 AppDelegate.h:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

在AppDelegate.m,修改didFinishLaunchingWithOptions方法如下:

#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
#include "AppDelegate.h"

@implementation AppDelegate

// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
  [self.flutterEngine runWithEntrypoint:nil];
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

如果 AppDelegate 已經(jīng)繼承于別的類的時(shí)候,可以通過讓你的 delegate 實(shí)現(xiàn)FlutterAppLifeCycleProvider協(xié)議:

#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@end

然后生命周期方法應(yīng)該由 FlutterPluginAppLifeCycleDelegate 來代理:

@implementation AppDelegate
{
    FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}

- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self]; // Only if you are using Flutter plugins.
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];

    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}

- (void)applicationDidEnterBackground:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillEnterForeground:application];
}

- (void)applicationWillResignActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillResignActive:application];
}

- (void)applicationDidBecomeActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidBecomeActive:application];
}

- (void)applicationWillTerminate:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillTerminate:application];
}

- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application
       didReceiveRemoteNotification:userInfo
             fetchCompletionHandler:completionHandler];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
    return [_lifeCycleDelegate application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
    return [_lifeCycleDelegate application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
  sourceApplication:(NSString*)sourceApplication
         annotation:(id)annotation {
    return [_lifeCycleDelegate application:application
                                   openURL:url
                         sourceApplication:sourceApplication
                                annotation:annotation];
}

- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}
@end

在 ViewController 中添加跳轉(zhuǎn)到 FlutterViewController 的測試代碼即可:

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(handleButtonAction)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Jump to flutterViewController" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor grayColor]];
    button.frame = CGRectMake(80.0, 210.0, 300.0, 40.0);
    button.center = self.view.center;
    [self.view addSubview:button];
}

- (void)handleButtonAction {
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    FlutterEngine *flutterEngine = delegate.flutterEngine;
    
    FlutterViewController *flutterVC = [[FlutterViewController alloc]initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterVC animated:YES completion:nil];
}
@end

5.使用熱重載的方式調(diào)試 Dart 代碼

熱重載指的是不用重新啟動(dòng)就看到修改后的效果,類似 web 網(wǎng)頁開發(fā)時(shí)保存就看到效果的方式。
進(jìn)入 flutter module,在終端執(zhí)行命令:

$ cd some/path/flutter_module
$ flutter run
flutter run

并且你能在控制臺(tái)中看下如下內(nèi)容:

??  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:54741/
For a more detailed help message, press "h". To quit, press "q".

你可以在 flutter_module 中編輯 Dart code,然后在終端輸入 r 來使用熱重載。你也可以在瀏覽器中輸入上面的 URL 來查看斷點(diǎn)、分析內(nèi)存和其他的調(diào)試任務(wù)。

集成 Flutter 后工程打包

1. flutter build ios

執(zhí)行flutter build ios以創(chuàng)建 release 版本(flutter build 默認(rèn)為--release,如需創(chuàng)建 debug 版本執(zhí)行flutter build ios —debug)。

2.成功后修改 Xcode 為 release 模式配置

3.最后選擇 Product > Archive 以生成構(gòu)建版本即可

archive 成功

混合工程改造優(yōu)化

Flutter 的工程結(jié)構(gòu)比較特殊,由 Flutter 目錄、Native 工程的目錄(即 iOS 和 Android 兩個(gè)目錄)組成。默認(rèn)情況下,引入了 Flutter 的 Native 工程無法脫離父目錄進(jìn)行獨(dú)立構(gòu)建和運(yùn)行,因?yàn)樗鼤?huì)反向依賴于 Flutter 相關(guān)的庫和資源。

實(shí)際上,在真實(shí)的開發(fā)情況下,開發(fā)者很少會(huì)創(chuàng)建一個(gè)完全 Flutter 的工程重寫項(xiàng)目,更多的情況是原生工程集成 Flutter。

1.問題

這樣就帶來了一系列問題:

(1)構(gòu)建打包問題:引入 Flutter 后,Native 工程因?qū)ζ溆辛艘蕾嚭婉詈?,從而無法獨(dú)立編譯構(gòu)建。在 Flutter 環(huán)境下,工程的構(gòu)建是從 Flutter 的構(gòu)建命令開始,執(zhí)行過程中包含了 Native 工程的構(gòu)建,開發(fā)者要配置完整的 Flutter 運(yùn)行環(huán)境才能走通整個(gè)流程

(2)混合編譯帶來的開發(fā)效率的降低:在轉(zhuǎn)型 Flutter 的過程中必然有許多業(yè)務(wù)仍使用 Native 進(jìn)行開發(fā),工程結(jié)構(gòu)的改動(dòng)會(huì)使開發(fā)無法在純 Native 環(huán)境下進(jìn)行,而適配到 Flutter 工程結(jié)構(gòu)對(duì)純 Native 開發(fā)來說又會(huì)造成不必要的構(gòu)建步驟,造成開發(fā)效率的降低。

2.目標(biāo)

希望能將 Flutter 依賴抽取出來,作為一個(gè) Flutter 依賴庫,供純 Native 工程引用,無需配置完整的 Flutter 環(huán)境。

3.Flutter 產(chǎn)物

iOS 工程對(duì) Flutter 有如下依賴:

  • Flutter.framework:Flutter 庫和引擎

  • App.framework:dart 業(yè)務(wù)源碼相關(guān)文件

  • flutter_assets:Flutter依賴的靜態(tài)資源,如字體,圖片等

  • Flutter Plugin:編譯出來的各種 plugin 的 framework

把以上依賴的編譯結(jié)果抽取出來,即是 Flutter 相關(guān)代碼的最終產(chǎn)物。

那么我們只需要將這些打包成一個(gè) SDK 依賴的形式提供給 Native 工程,就可以解除 Native 工程對(duì) Flutter 工程的直接依賴。

產(chǎn)物的產(chǎn)生:

對(duì) flutter 工程執(zhí)行 flutter build 命令后,生成在.ios/Flutter目錄下,直接手動(dòng)拷貝 framework 到主工程即可。

注意事項(xiàng):

framework 選擇 Create groups 加入文件夾,flutter_assets 選擇 Create folder references 加入文件夾。

add_in_project

加入完成后的結(jié)構(gòu):

thirdFramework

framework 加入后,記住一定要確認(rèn) framework 已在 TARGETS -> General -> Embedded Binaries 中添加完成。

embedded_binaires

最后改造 APPDelegate 即可:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate>

@property (strong, nonatomic) FlutterEngine *flutterEngine;

@end
#import "AppDelegate.h"

@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.flutterEngine = [[FlutterEngine alloc]initWithName:@"io.flutter" project:nil];
    [self.flutterEngine runWithEntrypoint:nil];
    return YES;
}

4. 優(yōu)化

為了更方便管理 framework,可以將這些文件上傳到遠(yuǎn)程倉庫,通過 CocoaPods 導(dǎo)入,Native 項(xiàng)目只需及時(shí)更新 pod 依賴即可。

我遇到過的一些問題及解決

1.在 Android Studio 上跑設(shè)備

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

選擇模擬器

提示你當(dāng)前有兩個(gè)模擬器設(shè)備,跑設(shè)備的時(shí)候要選擇運(yùn)行在哪個(gè)設(shè)備上,flutter run后面拼接上“-d <deviceId>”,deviceId 是第二列的內(nèi)容。

flutter run -d emulator-5554
flutter run -d C517D2D4-EAFA-42CA-B260-A18FA0ABFF60

電腦連著真機(jī)也同理,改成真機(jī)的 deviceId 即可。

2.flutter build ios 報(bào)錯(cuò)

build 時(shí)可能遇到的錯(cuò)誤:

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

build 時(shí)可能遇到的錯(cuò)誤

解決方法:

修改some/flutter_module/.ios/下 Runner 工程的 Bundle Identifier 和原生工程的一致,再次運(yùn)行flutter build ios即可。

3.開發(fā)時(shí)打包產(chǎn)物編譯失敗

當(dāng)你用flutter build ios的產(chǎn)物添加到原生工程中,跳轉(zhuǎn)到 Flutter 界面會(huì)黑屏并報(bào)出如下錯(cuò)誤:

flutter_build_questions

Failed to find snapshot: …/Library/Developer/CoreSimulator/Devices/…/data/Containers/Bundle/Application/…/FlutterMixDemo.app/Frameworks/App.framework/flutter_assets/kernel_blob.bin

如何解決:

調(diào)試模式下用flutter build ios —debug的產(chǎn)物,再次拖入工程即可。

原因:

首先我們對(duì)比下,執(zhí)行flutter build ios和執(zhí)行flutter build ios --debug.ios/Flutter/App.framework/flutter_assets的文件內(nèi)容:

flutter_build_ios.png

flutter_build_ios_debug.png

可以發(fā)現(xiàn),差別是在于三個(gè)文件:isolate_snapshot_data、kernel_blob.bin、vm_snapshot_data。

這里涉及 Flutter 的編譯模式知識(shí),具體可以參閱Flutter 的兩種編譯模式

Flutter 開發(fā)階段的編譯模式:使用了 Kernel Snapshot 模式編譯,打包產(chǎn)物中,可以發(fā)現(xiàn)幾樣?xùn)|西:

  • isolate_snapshot_data:用于加速 isolate 啟動(dòng),業(yè)務(wù)無關(guān)代碼,固定,僅和 flutter engine 版本有關(guān);

  • platform.dill:和 Dart VM 相關(guān)的 kernel 代碼,僅和 Dart 版本以及 engine 編譯版本有關(guān)。固定,業(yè)務(wù)無關(guān)代碼;

  • vm_snapshot_data:用于加速 Dart VM 啟動(dòng)的產(chǎn)物,業(yè)務(wù)無關(guān)代碼,僅和 flutter engine 版本有關(guān);

  • kernel_blob.bin:業(yè)務(wù)代碼產(chǎn)物 。

Flutter 生產(chǎn)階段的編譯模式:選擇了 AOT 打包。

4.集成后 Native 工程報(bào)錯(cuò)

Shell Script Invocation Error

line 2:/packages/flutter_tools/bin/xcode_backend.sh: No such file or directory

集成后 Native 工程報(bào)錯(cuò)

解決方法:

修改 TARGETS -> Build Setting -> FLUTTER_ROOT 為電腦安裝的 Flutter 環(huán)境的路徑即可。


集成后 Native 工程報(bào)錯(cuò)

5.如何在 iOS 工程 Debug 模式下使用 release 模式的 flutter

只需要將 Generated.xcconfig 中的 FLUTTER_BUILD_MODE 修改為 release,F(xiàn)LUTTER_FRAMEWORK_DIR 修改為 release 對(duì)應(yīng)的路徑即可。

其他

1.說明:

本文僅供用于學(xué)習(xí)參考,請(qǐng)勿用于商業(yè)用途。如需轉(zhuǎn)載,請(qǐng)標(biāo)明出處,謝謝合作。

本文系參考網(wǎng)絡(luò)公開 Flutter 學(xué)習(xí)資料以及個(gè)人學(xué)習(xí)體會(huì)總結(jié)所得,部分內(nèi)容為網(wǎng)絡(luò)公開學(xué)習(xí)資料,如有侵權(quán)請(qǐng)聯(lián)系作者刪除。

2.參考資料:

Flutter 中文網(wǎng):https://flutterchina.club

咸魚技術(shù)-flutter:https://www.yuque.com/xytech/flutter

iOS Native混編Flutter交互實(shí)踐:https://juejin.im/post/5bb033515188255c5e66f500#heading-3

Flutter混編之路——開發(fā)集成(iOS篇):http://www.itdecent.cn/p/48a9083ebe89

作者簡介

就職于甜橙金融(翼支付)信息技術(shù)部,負(fù)責(zé) iOS 客戶端開發(fā)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容