運(yùn)行一個(gè)原生的Flutter工程(也就是純Flutter)非常簡便,不過現(xiàn)在Flutter屬于試水階段,要是想在商業(yè)app中使用Flutter,目前基本上是將Flutter的頁面嵌入到目前先有的iOS或者安卓工程,目前講混合開發(fā)的文章有很多:
不過這些文章大多講的是安卓和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í)忽略)
并且在第一頁VC上增加一個(gè)Next按鈕,集成好Flutter以后,點(diǎn)擊Next可以進(jìn)入Flutter頁面
因?yàn)槲覀円迫雈lutter頁面,所以需要有navigation controller:
目前Flutter混合開發(fā)還不支持bit code,所以在iOS工程里關(guān)閉
2.3 Flutter Module搭建
這里有一個(gè)坑,按照flutter官方文檔,下載的flutter工具對應(yīng)其beta分支,是不支持生成Flutter module的,而混合開發(fā)的wiki里說,需要建立這么個(gè)module,通過咨詢大牛,需要切換到master分支,而flutter有個(gè)channel命令,可以切換工具分支:
如果你不在master分支,請執(zhí)行flutter channel master
之后在Flutter目錄下執(zhí)行flutter create -t module flutter_module
這樣就創(chuàng)建好了flutter module
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,
注意添加的路徑是HybridFlutter/Flutter/flutter_module
此時(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里添加)
添加內(nèi)容
#include "../../../Flutter/flutter_module/FlutterConfig.xcconfig"
然后,將Debug.xcconfig添加到iOS項(xiàng)目的Info-Configuration里:
2.4.2 AppFrameworkInfo.plist
這個(gè)文件在最新的flutter工具里已經(jīng)自動(dòng)創(chuàng)建好了
剛才我們看的文件目錄,不包含隱藏文件,其實(shí)flutter_module里還有對應(yīng)的ios和android插件工程,都是隱藏文件,從隱藏文件里可以看到AppFrameworkInfo.plist
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"后面
此時(shí)點(diǎn)擊xcode的運(yùn)行,會(huì)執(zhí)行到xcode-backend.sh腳本,所以不僅會(huì)編譯安裝iOS app到模擬器(暫時(shí)運(yùn)行對象是模擬器),而且在iOS工程目錄,也會(huì)生成一個(gè)Flutter文件夾,里面是Flutter工程的產(chǎn)物
把這些產(chǎn)物放到iOS工程里,就能獲取到flutter的資源了。
2.4.4 添加flutter編譯產(chǎn)物
,將iOS工程目錄下的Flutter文件夾添加到工程,然后確保文件夾下的兩個(gè)framework添加到Embeded Binaries里
確保flutter_aseets添加到Build Phases里的Copy Bundle Resources里
添加完,在工程目錄里,會(huì)多出一個(gè)flutter _aseets引用(注意只是引用,如果是拷貝可能會(huì)有問題),其實(shí)是引用的Flutter/flutter _aseets,試了半天沒有去掉,就先這樣吧
目前,所有的膠水文件都已經(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頁面了:
因?yàn)槲覀兊膶?dǎo)航欄使用了iOS原生的,所以flutter的導(dǎo)航欄有點(diǎn)多余了,我們?nèi)サ鬴lutter導(dǎo)航欄:
再次運(yùn)行:
證明改動(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文件夾
可以看到右上角已經(jīng)是可以run的狀態(tài)了,但是點(diǎn)擊的話,會(huì)有如下錯(cuò)誤提示:
原因很簡單,這個(gè)flutter_module不是一個(gè)獨(dú)立的工程,需要依賴一個(gè)app,所以我們需要先編譯出iOS app,并放到好找的位置:
點(diǎn)擊下圖的Edit Configurations
然后添加一個(gè)運(yùn)行前編譯app的命令,點(diǎn)擊下圖的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
,這里面指定了編譯的參數(shù)
添加后如圖:
接著添加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
的絕對路徑
大功告成,這時(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
真機(jī)的app和模擬機(jī)app的產(chǎn)物路徑不一樣,所以flutter參數(shù)也得變:
--use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphoneos/HybridIOS.app
這樣,我們就可以選擇想要運(yùn)行的是真機(jī)還是模擬器,然后點(diǎn)擊run運(yùn)行
5 總結(jié)
flutter混合開發(fā),需要手動(dòng)設(shè)置的地方很多,但是一旦設(shè)置好,就不需要再改動(dòng),至于最后的flutter運(yùn)行參數(shù),需要指定絕對路徑,不知道什么原因,好在影響不大,有空再仔細(xì)研究。希望本文會(huì)對你有幫助