運行一個原生的Flutter工程(也就是純Flutter)很是簡便,不過如今Flutter屬於試水階段,要是想在商業app中使用Flutter,目前基本上是將Flutter的頁面嵌入到目前先有的iOS或者安卓工程,目前講混合開發的文章有不少:android
Flutter混合工程開發探究github
不過這些文章大多講的是安卓和flutter混合開發的,沒有iOS和Flutter混合開發的比較詳細的步驟實操,上週試了一下iOS和Flutter混合,有一些坑,總結給你們bash
既然用Flutter混合開發,那確定是但願寫一套代碼,安卓iOS都能無負擔運行,因此在開發的時候,須要知足以下需求:app
混合開發最權威的指南固然是flutter本身的wiki,可是缺陷是iOS部分,自動運行腳本的內容不夠詳細,項目結構也不利於混合開發,本文以其爲基礎,又對目錄結構和腳本作了一些修改,使其便於維護iphone
HybridFlutter
|-iOS
|-Android
|-Flutter
|-build
複製代碼
創建完了上圖文件目錄,添加iOS工程(安卓工程暫時忽略)ide
而且在第一頁VC上增長一個Next按鈕,集成好Flutter之後,點擊Next能夠進入Flutter頁面工具
由於咱們要推入flutter頁面,因此須要有navigation controller:
目前Flutter混合開發還不支持bit code,因此在iOS工程裏關閉
這裏有一個坑,按照flutter官方文檔,下載的flutter工具對應其beta分支,是不支持生成Flutter module的,而混合開發的wiki裏說,須要創建這麼個module,經過諮詢大牛,須要切換到master分支,而flutter有個channel命令,能夠切換工具分支:
若是你不在master分支,請執行flutter channel master
以後在Flutter目錄下執行flutter create -t module flutter_module
這樣就建立好了flutter module
混合開發最關鍵的是將兩個項目銜接起來,因此須要一些配置
首先是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裏:
這個文件在最新的flutter工具裏已經自動建立好了 剛纔咱們看的文件目錄,不包含隱藏文件,其實flutter_module裏還有對應的ios和android插件工程,都是隱藏文件,從隱藏文件裏能夠看到AppFrameworkInfo.plist
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的資源了。
,將iOS工程目錄下的Flutter文件夾添加到工程,而後確保文件夾下的兩個framework添加到Embeded Binaries裏
確保flutter_aseets添加到Build Phases裏的Copy Bundle Resources裏添加完,在工程目錄裏,會多出一個flutter _aseets引用(注意只是引用,若是是拷貝可能會有問題),實際上是引用的Flutter/flutter _aseets,試了半天沒有去掉,就先這樣吧
目前,全部的膠水文件都已經添加完了,下一步就是在iOS工程裏,顯示flutter頁面
改變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官方也會更新相關的方法
在首頁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
你可能發現了,上面的代碼運行的時候,在flutter頁面點擊右下角的加號能夠增長中間的數字,可是當退出當前頁面,再進入flutter頁面之後,中間的數字又重置爲0了,這是由於每次點擊Next,都會從新分配和初始化全部flutter資源,這形成了flutter頁面啓動慢,狀態沒法保存(這個頁面的數字狀態不必保存,可是別的場景下必定有須要保存的內容)
因此Flutter新銳專家之路:混合開發篇對混合開發中flutter部分作了很好的管理,它將flutter部分作成單例,使其基礎資源在app運行期間只運行一次,再將flutter根頁面設置成一個空白container,須要flutter推入什麼頁面,就發消息給flutter,flutter在空白container基礎上推入對應頁面,這樣當從flutter的某個頁面回退到iOS原生頁面的時候,flutter也會釋放掉剛剛顯示的頁面,回退到空白頁面。
針對怎麼寫代碼,不是這篇文章的範疇,下面說說混合開發最後的一個痛點
如今的工程,flutter部分有改動,能夠直接經過綁定的xcode-backend.sh
來編譯,並生成framework和資源文件,因此不管是iOS端,仍是flutter端有改動,在xcode上點擊run均可以運行到模擬器和真機,並且iOS和flutter項目代碼彼此獨立,只有flutter的編譯產物留在了iOS文件夾裏 可是如今還有一個問題,就是當開發flutter部分的時候,咱們並不想碰xcode,最好能關掉xcode,只打開android studio作開發,而後點擊AS上的run按鈕運行。
經過上述兩步,就能夠在android studio裏,直接往iOS系統裏安裝混合app了
用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
真機是同樣的原理,就是命令參數不同:
運行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運行
flutter混合開發,須要手動設置的地方不少,可是一旦設置好,就不須要再改動,至於最後的flutter運行參數,須要指定絕對路徑,不知道什麼緣由,好在影響不大,有空再仔細研究。但願本文會對你有幫助