執行一個原生的Flutter工程(也就是純Flutter)非常簡便,不過現在Flutter屬於試水階段,要是想在商業app中使用Flutter,目前基本上是將Flutter的頁面嵌入到目前先有的iOS或者安卓工程,目前講混合開發的文章有很多:
不過這些文章大多講的是安卓和flutter混合開發的,沒有iOS和Flutter混合開發的比較詳細的步驟實操,上週試了一下iOS和Flutter混合,有一些坑,總結給大家
1.目的
既然用Flutter混合開發,那肯定是希望寫一套程式碼,安卓iOS都能無負擔執行,所以在開發的時候,需要滿足如下需求:
- Flutter、iOS、安卓工程的目錄在同一級,互相之前平級、無巢狀
- 開發iOS的時候,不用操心Flutter部分,只用xcode點選執行就可以(即修改編譯iOS專案時,使用編譯好的Flutter產物)
- 開發Flutter的時候,不用操心iOS部分,只用android studio點選執行就可以
- 支援模擬器和真機
混合開發最權威的指南當然是flutter自己的wiki,但是缺陷是iOS部分,自動執行指令碼的內容不夠詳細,專案結構也不利於混合開發,本文以其為基礎,又對目錄結構和指令碼做了一些修改,使其便於維護
2.專案搭建
2.1 檔案目錄搭建
HybridFlutter
|-iOS
|-Android
|-Flutter
|-build
複製程式碼
2.2 iOS專案搭建
建立完了上圖檔案目錄,新增iOS工程(安卓工程暫時忽略)
並且在第一頁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執行引數,需要指定絕對路徑,不知道什麼原因,好在影響不大,有空再仔細研究。希望本文會對你有幫助