Flutter與已有iOS工程混合開發與腳本配置 | 掘金技術徵文

運行一個原生的Flutter工程(也就是純Flutter)很是簡便,不過如今Flutter屬於試水階段,要是想在商業app中使用Flutter,目前基本上是將Flutter的頁面嵌入到目前先有的iOS或者安卓工程,目前講混合開發的文章有不少:android

Flutter新銳專家之路:混合開發篇ios

Flutter混合工程改造實踐git

Flutter混合工程開發探究github

Now直播iOS Flutter混合工程實踐xcode

不過這些文章大多講的是安卓和flutter混合開發的,沒有iOS和Flutter混合開發的比較詳細的步驟實操,上週試了一下iOS和Flutter混合,有一些坑,總結給你們bash

1.目的

既然用Flutter混合開發,那確定是但願寫一套代碼,安卓iOS都能無負擔運行,因此在開發的時候,須要知足以下需求:app

  • Flutter、iOS、安卓工程的目錄在同一級,互相以前平級、無嵌套
  • 開發iOS的時候,不用操心Flutter部分,只用xcode點擊運行就能夠(即修改編譯iOS項目時,使用編譯好的Flutter產物)
  • 開發Flutter的時候,不用操心iOS部分,只用android studio點擊運行就能夠
  • 支持模擬器和真機

混合開發最權威的指南固然是flutter本身的wiki,可是缺陷是iOS部分,自動運行腳本的內容不夠詳細,項目結構也不利於混合開發,本文以其爲基礎,又對目錄結構和腳本作了一些修改,使其便於維護iphone

2.項目搭建

2.1 文件目錄搭建

HybridFlutter
    |-iOS
    |-Android
    |-Flutter
    |-build
複製代碼

2.2 iOS項目搭建

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

而且在第一頁VC上增長一個Next按鈕,集成好Flutter之後,點擊Next能夠進入Flutter頁面工具

由於咱們要推入flutter頁面,因此須要有navigation controller:

目前Flutter混合開發還不支持bit code,因此在iOS工程裏關閉

2.3 Flutter Module搭建

這裏有一個坑,按照flutter官方文檔,下載的flutter工具對應其beta分支,是不支持生成Flutter module的,而混合開發的wiki裏說,須要創建這麼個module,經過諮詢大牛,須要切換到master分支,而flutter有個channel命令,能夠切換工具分支:

若是你不在master分支,請執行flutter channel master

以後在Flutter目錄下執行flutter create -t module flutter_module

這樣就建立好了flutter module

目前爲止的目錄結構

2.4 添加膠水文件

混合開發最關鍵的是將兩個項目銜接起來,因此須要一些配置

2.4.1 xcconfig文件

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

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

此時可能xcode會在ios工程裏添加了一個FlutterConfig.xcconfig文件的引用,爲了項目乾淨,能夠刪除這個引用(可是不要刪除文件)

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

上面是給flutter添加xcconfig文件,下載添加ios工程裏的xccofig文件Debug.xcconfig,並引用FlutterConfig.xcconfig(若是iOS工程裏已經有了xcconfig文件,那麼直接在已有的xcconfig裏添加)

添加內容 #include "../../../Flutter/flutter_module/FlutterConfig.xcconfig"

而後,將Debug.xcconfig添加到iOS項目的Info-Configuration裏:

2.4.2 AppFrameworkInfo.plist

這個文件在最新的flutter工具裏已經自動建立好了 剛纔咱們看的文件目錄,不包含隱藏文件,其實flutter_module裏還有對應的ios和android插件工程,都是隱藏文件,從隱藏文件裏能夠看到AppFrameworkInfo.plist

2.4.3 引入xcode-backend.sh

在ios工程裏添加運行腳本"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build,而且確保Run Script這一行在 "Target dependencies" 或者 "Check Pods Manifest.lock"後面

此時點擊xcode的運行,會執行到xcode-backend.sh腳本,因此不只會編譯安裝iOS app到模擬器(暫時運行對象是模擬器),並且在iOS工程目錄,也會生成一個Flutter文件夾,裏面是Flutter工程的產物

把這些產物放到iOS工程裏,就能獲取到flutter的資源了。

2.4.4 添加flutter編譯產物

,將iOS工程目錄下的Flutter文件夾添加到工程,而後確保文件夾下的兩個framework添加到Embeded Binaries裏

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

添加完,在工程目錄裏,會多出一個flutter _aseets引用(注意只是引用,若是是拷貝可能會有問題),實際上是引用的Flutter/flutter _aseets,試了半天沒有去掉,就先這樣吧

目前,全部的膠水文件都已經添加完了,下一步就是在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


複製代碼

這部分改造的原理尚未深究,並且有一些方法的實現iOS已經提示棄用了,你們在加入已有工程的時候,須要酌情考慮,我相信後續flutter官方也會更新相關的方法

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]];//消息發送代碼,本文不作解釋
    __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頁面確定會報錯,這和flutter不要緊,若是確實沒有navigation controller,能夠present flutterViewController

運行代碼,點擊next,就能夠看到flutter頁面了:

由於咱們的導航欄使用了iOS原生的,因此flutter的導航欄有點多餘了,咱們去掉flutter導航欄:

再次運行:

證實改動能夠同步到app

3.3 flutter頁面管理

你可能發現了,上面的代碼運行的時候,在flutter頁面點擊右下角的加號能夠增長中間的數字,可是當退出當前頁面,再進入flutter頁面之後,中間的數字又重置爲0了,這是由於每次點擊Next,都會從新分配和初始化全部flutter資源,這形成了flutter頁面啓動慢,狀態沒法保存(這個頁面的數字狀態不必保存,可是別的場景下必定有須要保存的內容)

因此Flutter新銳專家之路:混合開發篇對混合開發中flutter部分作了很好的管理,它將flutter部分作成單例,使其基礎資源在app運行期間只運行一次,再將flutter根頁面設置成一個空白container,須要flutter推入什麼頁面,就發消息給flutter,flutter在空白container基礎上推入對應頁面,這樣當從flutter的某個頁面回退到iOS原生頁面的時候,flutter也會釋放掉剛剛顯示的頁面,回退到空白頁面。

4. 配置自動運行腳本

針對怎麼寫代碼,不是這篇文章的範疇,下面說說混合開發最後的一個痛點

如今的工程,flutter部分有改動,能夠直接經過綁定的xcode-backend.sh來編譯,並生成framework和資源文件,因此不管是iOS端,仍是flutter端有改動,在xcode上點擊run均可以運行到模擬器和真機,並且iOS和flutter項目代碼彼此獨立,只有flutter的編譯產物留在了iOS文件夾裏 可是如今還有一個問題,就是當開發flutter部分的時候,咱們並不想碰xcode,最好能關掉xcode,只打開android studio作開發,而後點擊AS上的run按鈕運行。

4.1 實現原理

  • xcode命令行工具,能夠編譯iOS項目(就像xcode裏點擊run同樣),而且還能指定生成.app文件的目錄
  • flutter運行的時候,能夠指定--use-application-binary,flutter編譯產物,以hot-load的方式注入到指定app中(這個原理是我本身猜的,實際狀況待仔細確認)

經過上述兩步,就能夠在android studio裏,直接往iOS系統裏安裝混合app了

4.2 模擬器實現

用android studio打開flutter_module文件夾

能夠看到右上角已是能夠run的狀態了,可是點擊的話,會有以下錯誤提示:

緣由很簡單,這個flutter_module不是一個獨立的工程,須要依賴一個app,因此咱們須要先編譯出iOS app,並放到好找的位置:

點擊下圖的Edit Configurations

而後添加一個運行前編譯app的命令,點擊下圖的Run External tool

添加下面的一條:

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,這裏面指定了編譯的參數

添加後如圖:

接着添加flutter編譯的參數,指定剛剛編譯出來的app做爲hotload的宿主app: --use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app 這裏須要注意,我一開始使用相對路徑,怎麼也運行不起來,說找不到對應的app,因此我使用了絕對路徑,你要換成本身的HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app的絕對路徑

大功告成,這時候點擊run運行,就會先編譯ipa,在運行flutter

4.3 真機

真機是同樣的原理,就是命令參數不同:

運行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

真機的app和模擬機app的產物路徑不同,因此flutter參數也得變: --use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphoneos/HybridIOS.app

這樣,咱們就能夠選擇想要運行的是真機仍是模擬器,而後點擊run運行

5 總結

flutter混合開發,須要手動設置的地方不少,可是一旦設置好,就不須要再改動,至於最後的flutter運行參數,須要指定絕對路徑,不知道什麼緣由,好在影響不大,有空再仔細研究。但願本文會對你有幫助

項目GitHub

從 0 到 1:個人 Flutter 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索