本文轉自⬆️, 並結合本身的實踐對其中一些地方作修改, 使用 Swift 語言.android
運行一個原生的Flutter工程(也就是純Flutter)很是簡便,不過如今Flutter屬於試水階段,要是想在商業app中使用Flutter,目前基本上是將Flutter的頁面嵌入到目前先有的iOS或者安卓工程,目前講混合開發的文章有不少:ios
Flutter混合工程改造實踐github
Flutter混合工程開發探究xcode
不過這些文章大多講的是安卓和flutter混合開發的,沒有iOS和Flutter混合開發的比較詳細的步驟實操,上週試了一下iOS和Flutter混合,有一些坑,總結給你們app
既然用Flutter混合開發,那確定是但願寫一套代碼,安卓iOS都能無負擔運行,因此在開發的時候,須要知足以下需求:iphone
混合開發最權威的指南固然是flutter本身的wiki,可是缺陷是iOS部分,自動運行腳本的內容不夠詳細,項目結構也不利於混合開發,本文以其爲基礎,又對目錄結構和腳本作了一些修改,使其便於維護ide
2.項目搭建
函數
HybridFlutter
|-iOS
|-Android
|-Flutter
|-build
複製代碼複製代碼
創建完了上圖文件目錄,添加iOS工程(安卓工程暫時忽略)
而且在第一頁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,
在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
在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引用(注意只是引用,若是是拷貝可能會有問題),實際上是引用的Flutter/flutter _aseets,試了半天沒有去掉,就先這樣吧
目前,全部的膠水文件都已經添加完了,下一步就是在iOS工程裏,顯示flutter頁面
import UIKit
import Flutter
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
override func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
override func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
override func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
override func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
override func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
複製代碼
改變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運行參數,須要指定絕對路徑,不知道什麼緣由,好在影響不大,有空再仔細研究。但願本文會對你有幫助