Flutter與已有iOS工程混合開發(fā)與腳本配置

運(yùn)行一個(gè)原生的Flutter工程(也就是純Flutter)非常簡便,不過現(xiàn)在Flutter屬于試水階段,要是想在商業(yè)app中使用Flutter,目前基本上是將Flutter的頁面嵌入到目前先有的iOS或者安卓工程,目前講混合開發(fā)的文章有很多:

Flutter新銳專家之路:混合開發(fā)篇

Flutter混合工程改造實(shí)踐

Flutter混合工程開發(fā)探究

Now直播iOS Flutter混合工程實(shí)踐

不過這些文章大多講的是安卓和flutter混合開發(fā)的,沒有iOS和Flutter混合開發(fā)的比較詳細(xì)的步驟實(shí)操,上周試了一下iOS和Flutter混合,有一些坑,總結(jié)給大家

1.目的

既然用Flutter混合開發(fā),那肯定是希望寫一套代碼,安卓iOS都能無負(fù)擔(dān)運(yùn)行,所以在開發(fā)的時(shí)候,需要滿足如下需求:

  • Flutter、iOS、安卓工程的目錄在同一級,互相之前平級、無嵌套
  • 開發(fā)iOS的時(shí)候,不用操心Flutter部分,只用xcode點(diǎn)擊運(yùn)行就可以(即修改編譯iOS項(xiàng)目時(shí),使用編譯好的Flutter產(chǎn)物)
  • 開發(fā)Flutter的時(shí)候,不用操心iOS部分,只用android studio點(diǎn)擊運(yùn)行就可以
  • 支持模擬器和真機(jī)

混合開發(fā)最權(quán)威的指南當(dāng)然是flutter自己的wiki,但是缺陷是iOS部分,自動(dòng)運(yùn)行腳本的內(nèi)容不夠詳細(xì),項(xiàng)目結(jié)構(gòu)也不利于混合開發(fā),本文以其為基礎(chǔ),又對目錄結(jié)構(gòu)和腳本做了一些修改,使其便于維護(hù)

2.項(xiàng)目搭建

2.1 文件目錄搭建

HybridFlutter
    |-iOS
    |-Android
    |-Flutter
    |-build

2.2 iOS項(xiàng)目搭建

建立完了上圖文件目錄,添加iOS工程(安卓工程暫時(shí)忽略)

image

并且在第一頁VC上增加一個(gè)Next按鈕,集成好Flutter以后,點(diǎn)擊Next可以進(jìn)入Flutter頁面

image

因?yàn)槲覀円迫雈lutter頁面,所以需要有navigation controller:

image

目前Flutter混合開發(fā)還不支持bit code,所以在iOS工程里關(guān)閉

image

2.3 Flutter Module搭建

這里有一個(gè)坑,按照flutter官方文檔,下載的flutter工具對應(yīng)其beta分支,是不支持生成Flutter module的,而混合開發(fā)的wiki里說,需要建立這么個(gè)module,通過咨詢大牛,需要切換到master分支,而flutter有個(gè)channel命令,可以切換工具分支:

image

如果你不在master分支,請執(zhí)行flutter channel master

之后在Flutter目錄下執(zhí)行flutter create -t module flutter_module

image

這樣就創(chuàng)建好了flutter module

目前為止的目錄結(jié)構(gòu)

2.4 添加膠水文件

混合開發(fā)最關(guān)鍵的是將兩個(gè)項(xiàng)目銜接起來,所以需要一些配置

2.4.1 xcconfig文件

首先是xcode工程配置的銜接,打開ios工程,在xcode中點(diǎn)擊File->New->File添加Configuration Settings File文件,命名為FlutterConfig.xcconfig,

image

注意添加的路徑是HybridFlutter/Flutter/flutter_module

image

此時(shí)可能xcode會(huì)在ios工程里添加了一個(gè)FlutterConfig.xcconfig文件的引用,為了項(xiàng)目干凈,可以刪除這個(gè)引用(但是不要?jiǎng)h除文件)

在FlutterConfig.xcconfig里添加
#include "./.ios/Flutter/Generated.xcconfig"
引用flutter_module下的ios插件里的Generated.xcconfig文件

上面是給flutter添加xcconfig文件,下載添加ios工程里的xccofig文件Debug.xcconfig,并引用FlutterConfig.xcconfig(如果iOS工程里已經(jīng)有了xcconfig文件,那么直接在已有的xcconfig里添加)

image

添加內(nèi)容#include "../../../Flutter/flutter_module/FlutterConfig.xcconfig"

image

然后,將Debug.xcconfig添加到iOS項(xiàng)目的Info-Configuration里:

image

2.4.2 AppFrameworkInfo.plist

這個(gè)文件在最新的flutter工具里已經(jīng)自動(dòng)創(chuàng)建好了
剛才我們看的文件目錄,不包含隱藏文件,其實(shí)flutter_module里還有對應(yīng)的ios和android插件工程,都是隱藏文件,從隱藏文件里可以看到AppFrameworkInfo.plist

image

2.4.3 引入xcode-backend.sh

在ios工程里添加運(yùn)行腳本"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build,并且確保Run Script這一行在 "Target dependencies" 或者 "Check Pods Manifest.lock"后面

image

此時(shí)點(diǎn)擊xcode的運(yùn)行,會(huì)執(zhí)行到xcode-backend.sh腳本,所以不僅會(huì)編譯安裝iOS app到模擬器(暫時(shí)運(yùn)行對象是模擬器),而且在iOS工程目錄,也會(huì)生成一個(gè)Flutter文件夾,里面是Flutter工程的產(chǎn)物

image

把這些產(chǎn)物放到iOS工程里,就能獲取到flutter的資源了。

2.4.4 添加flutter編譯產(chǎn)物

,將iOS工程目錄下的Flutter文件夾添加到工程,然后確保文件夾下的兩個(gè)framework添加到Embeded Binaries里

image

確保flutter_aseets添加到Build Phases里的Copy Bundle Resources里

image

添加完,在工程目錄里,會(huì)多出一個(gè)flutter _aseets引用(注意只是引用,如果是拷貝可能會(huì)有問題),其實(shí)是引用的Flutter/flutter _aseets,試了半天沒有去掉,就先這樣吧

image

目前,所有的膠水文件都已經(jīng)添加完了,下一步就是在iOS工程里,顯示flutter頁面

3. 引用Flutter頁面

3.1 AppDelegate改造

改變AppDelegate.h,使其父類指向FlutterAppDelegate:

#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@end

改造AppDelegate.m

//
//  AppDelegate.m
//  HybridIOS
//
//  Created by Realank on 2018/8/20.
//  Copyright ? 2018年 Realank. All rights reserved.
//

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

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

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    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


這部分改造的原理還沒有深究,而且有一些方法的實(shí)現(xiàn)iOS已經(jīng)提示棄用了,大家在加入已有工程的時(shí)候,需要酌情考慮,我相信后續(xù)flutter官方也會(huì)更新相關(guān)的方法

3.2 推入flutter頁面

在首頁VC中添加如下代碼

//
//  ViewController.m
//  HybridIOS
//
//  Created by Realank on 2018/8/20.
//  Copyright ? 2018年 Realank. All rights reserved.
//

#import "ViewController.h"
#import <Flutter/Flutter.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (IBAction)goNext:(id)sender {
    FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
    FlutterBasicMessageChannel* messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"channel"
                                                        binaryMessenger:flutterViewController
                                                                  codec:[FlutterStandardMessageCodec sharedInstance]];//消息發(fā)送代碼,本文不做解釋
    __weak __typeof(self) weakSelf = self;
    [messageChannel setMessageHandler:^(id message, FlutterReply reply) {
        // Any message on this channel pops the Flutter view.
        [[weakSelf navigationController] popViewControllerAnimated:YES];
        reply(@"");
    }];
    NSAssert([self navigationController], @"Must have a NaviationController");
    [[self navigationController]  pushViewController:flutterViewController animated:YES];
}

@end

如果你的首頁不在navigation controller里,那么pushflutter頁面肯定會(huì)報(bào)錯(cuò),這和flutter沒關(guān)系,如果確實(shí)沒有navigation controller,可以present flutterViewController

運(yùn)行代碼,點(diǎn)擊next,就可以看到flutter頁面了:

image

因?yàn)槲覀兊膶?dǎo)航欄使用了iOS原生的,所以flutter的導(dǎo)航欄有點(diǎn)多余了,我們?nèi)サ鬴lutter導(dǎo)航欄:

image

再次運(yùn)行:

image

證明改動(dòng)可以同步到app

3.3 flutter頁面管理

你可能發(fā)現(xiàn)了,上面的代碼運(yùn)行的時(shí)候,在flutter頁面點(diǎn)擊右下角的加號可以增加中間的數(shù)字,但是當(dāng)退出當(dāng)前頁面,再進(jìn)入flutter頁面以后,中間的數(shù)字又重置為0了,這是因?yàn)槊看吸c(diǎn)擊Next,都會(huì)重新分配和初始化所有flutter資源,這造成了flutter頁面啟動(dòng)慢,狀態(tài)無法保存(這個(gè)頁面的數(shù)字狀態(tài)沒必要保存,但是別的場景下一定有需要保存的內(nèi)容)

所以Flutter新銳專家之路:混合開發(fā)篇對混合開發(fā)中flutter部分做了很好的管理,它將flutter部分做成單例,使其基礎(chǔ)資源在app運(yùn)行期間只運(yùn)行一次,再將flutter根頁面設(shè)置成一個(gè)空白container,需要flutter推入什么頁面,就發(fā)消息給flutter,flutter在空白container基礎(chǔ)上推入對應(yīng)頁面,這樣當(dāng)從flutter的某個(gè)頁面回退到iOS原生頁面的時(shí)候,flutter也會(huì)釋放掉剛剛顯示的頁面,回退到空白頁面。

4. 配置自動(dòng)運(yùn)行腳本

針對怎么寫代碼,不是這篇文章的范疇,下面說說混合開發(fā)最后的一個(gè)痛點(diǎn)

現(xiàn)在的工程,flutter部分有改動(dòng),可以直接通過綁定的xcode-backend.sh來編譯,并生成framework和資源文件,所以無論是iOS端,還是flutter端有改動(dòng),在xcode上點(diǎn)擊run都可以運(yùn)行到模擬器和真機(jī),而且iOS和flutter項(xiàng)目代碼彼此獨(dú)立,只有flutter的編譯產(chǎn)物留在了iOS文件夾里
但是現(xiàn)在還有一個(gè)問題,就是當(dāng)開發(fā)flutter部分的時(shí)候,我們并不想碰xcode,最好能關(guān)掉xcode,只打開android studio做開發(fā),然后點(diǎn)擊AS上的run按鈕運(yùn)行。

4.1 實(shí)現(xiàn)原理

  • xcode命令行工具,可以編譯iOS項(xiàng)目(就像xcode里點(diǎn)擊run一樣),并且還能指定生成.app文件的目錄
  • flutter運(yùn)行的時(shí)候,可以指定--use-application-binary,flutter編譯產(chǎn)物,以hot-load的方式注入到指定app中(這個(gè)原理是我自己猜的,實(shí)際情況待仔細(xì)確認(rèn))

通過上述兩步,就可以在android studio里,直接往iOS系統(tǒng)里安裝混合app了

4.2 模擬器實(shí)現(xiàn)

用android studio打開flutter_module文件夾

image

可以看到右上角已經(jīng)是可以run的狀態(tài)了,但是點(diǎn)擊的話,會(huì)有如下錯(cuò)誤提示:

image

原因很簡單,這個(gè)flutter_module不是一個(gè)獨(dú)立的工程,需要依賴一個(gè)app,所以我們需要先編譯出iOS app,并放到好找的位置:

點(diǎn)擊下圖的Edit Configurations


image

然后添加一個(gè)運(yùn)行前編譯app的命令,點(diǎn)擊下圖的Run External tool


image

添加下面的一條:

image

Program里填/usr/bin/env,Arguments里填xcrun xcodebuild build -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -project ../../iOS/HybridIOS/HybridIOS.xcodeproj -scheme HybridIOS BUILD_DIR=../build/ios -sdk iphonesimulator -arch x86_64,這里面指定了編譯的參數(shù)

添加后如圖:

image

接著添加flutter編譯的參數(shù),指定剛剛編譯出來的app作為hotload的宿主app:
--use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app
這里需要注意,我一開始使用相對路徑,怎么也運(yùn)行不起來,說找不到對應(yīng)的app,所以我使用了絕對路徑,你要換成自己的HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app的絕對路徑

image

大功告成,這時(shí)候點(diǎn)擊run運(yùn)行,就會(huì)先編譯ipa,在運(yùn)行flutter

4.3 真機(jī)

真機(jī)是一樣的原理,就是命令參數(shù)不一樣:

運(yùn)行flutter前編譯app的命令:xcrun xcodebuild build -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -project ../../iOS/HybridIOS/HybridIOS.xcodeproj -scheme HybridIOS BUILD_DIR=../build/ios -sdk iphoneos -arch arm64

image

真機(jī)的app和模擬機(jī)app的產(chǎn)物路徑不一樣,所以flutter參數(shù)也得變:
--use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphoneos/HybridIOS.app

image
image

這樣,我們就可以選擇想要運(yùn)行的是真機(jī)還是模擬器,然后點(diǎn)擊run運(yùn)行

5 總結(jié)

flutter混合開發(fā),需要手動(dòng)設(shè)置的地方很多,但是一旦設(shè)置好,就不需要再改動(dòng),至于最后的flutter運(yùn)行參數(shù),需要指定絕對路徑,不知道什么原因,好在影響不大,有空再仔細(xì)研究。希望本文會(huì)對你有幫助

項(xiàng)目GitHub

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內(nèi)容