集成
系統要求
你的開發環境必須滿足 Flutter 對 macOS 系統的版本要求1 并 已經安裝 Xcode2,Flutter 支持 iOS 8.0 及以上。
創建 Flutter module
為了將 Flutter 集成到你的既有應用里,第一步要創建一個 Flutter module。
在命令行中執行:
cd some/path/flutter
create --template module my_flutter
Flutter module 會創建在 some/path/my_flutter/
目錄。在這個目錄中,你可以像在其它 Flutter 項目中一樣,執行 flutter
命令。比如 flutter run --debug
或者 flutter build ios
。
你也同樣可以在 Android Studio/IntelliJ3 或者 VS Code4 中運行這個模塊,并附帶 Flutter 和 Dart 插件。在集成到既有應用前,這個項目在 Flutter module 中包含了一個單視圖的示例代碼,對 Flutter 側代碼的測試會有幫助。
模塊組織
在 my_flutter
模塊,目錄結構和普通 Flutter 應用類似:
my_flutter/
├─.ios/│
├─Runner.xcworkspace│
└─Flutter/podhelper.rb├─lib/│
└─main.dart├─test/
└─pubspec.yaml
添加你的 Dart 代碼到 lib/
目錄。
添加 Flutter 依賴到 my_flutter/pubspec.yaml
,包括 Flutter packages 和 plugins。
.ios/
隱藏文件夾包含了一個 Xcode workspace,用于單獨運行你的 Flutter module。它是一個獨立啟動 Flutter 代碼的殼工程,并且包含了一個幫助腳本,用于編譯 framewroks 或者使用 CocoaPods 將 Flutter module 集成到你的既有應用。
iOS 代碼要添加到你的既有應用或者 Flutter plugin 中,而不是 Flutter module 的
.ios/
目錄下。.ios/
下的改變不會集成到你的既有應用。在my_flutter
執行flutter clean
或者flutter pub get
會重新生成這個目錄。
在你的既有應用中集成 Flutter module
這里有兩種方式可以將 Flutter 集成到你的既有應用中。
? 使用 CocoaPods 依賴管理和已安裝的 Flutter SDK 。(推薦)
? 把 Flutter engine 、你的 dart 代碼和所有 Flutter plugin 編譯成 framework 。然后用 Xcode 手動集成到你的應用中,并更新編譯設置。
你的應用將不能在模擬器上運行 Release 模式,因為 Flutter 還不支持將 Dart 代碼編譯成 x86 ahead-of-time (AOT) 模式的二進制文件。你可以在模擬機和真機上運行 Debug 模式,在真機上運行 Release 模式。
使用 Flutter 會 增加應用體積5。
選項 A - 使用 CocoaPods 和 Flutter SDK 集成
這個方法需要你的項目的所有開發者,都在本地安裝 Flutter SDK。只需要在 Xcode 中編譯應用,就可以自動運行腳本來集成 dart 代碼和 plugin。這個方法允許你使用 Flutter module 中的最新代碼快速迭代開發,而無需在 Xcode 以外執行額外的命令。
下面的示例假設你的既有應用和 Flutter module 在相鄰目錄。如果你有不同的目錄結構,需要適配到對應的路徑。
some/path/├── my_flutter/│
└── .ios/│
└── Flutter/│
└── podhelper.rb
└── MyApp/
└── Podfile
如果你的應用(MyApp
)還沒有 Podfile,根據 CocoaPods getting started guide 來在項目中添加 Podfile
。
? 在 Podfile
中添加下面代碼:
# MyApp/Podfile flutter_application_path = '../my_flutter'load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
? 每個需要集成 Flutter 的 Podfile target,執行install_all_flutter_pods(flutter_application_path)
:
# MyApp/Podfile target 'MyApp' do install_all_flutter_pods(flutter_application_path)end
? 運行 pod install
。
當你在
my_flutter/pubspec.yaml
改變了 Flutter plugin 依賴,需要在 Flutter module 目錄運行flutter pub get
,來更新會被podhelper.rb
腳本用到的 plugin 列表,然后再次在你的應用目錄some/path/MyApp
運行pod install
.
podhelper.rb
腳本會把你的 plugins,Flutter.framework
,和 App.framework
集成到你的項目中。
你應用的 Debug 和 Release 編譯配置,將會集成相對應的 Debug 或 Release 的 編譯產物6。
可以增加一個 Profile 編譯配置用于在 profile 模式下測試應用。
Flutter.framework
是 Flutter engine 的框架,App.framework
是你的 Dart 代碼的編譯產物。
在 Xcode 中打開 MyApp.xcworkspace
,你現在可以使用 ?B
編譯項目了。
選項 B - 在 Xcode 中集成 frameworks
除了上面的方法,你也可以創建必備的 frameworks,手動修改既有 Xcode 項目,將他們集成進去。當你組內其它成員們不能在本地安裝 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作為既有應用的依賴管理時,這種方法會比較合適。但是每當你在 Flutter module 中改變了代碼,都必須運行 flutter build ios-framework
。
如果你使用前面的 “使用 CocoaPods 和 Flutter SDK 集成” ,你可以跳過本步驟。
下面的示例假設你想在 some/path/MyApp/Flutter/
目錄下創建 frameworks:
flutter build ios-framework --output=some/path/MyApp/Flutter/
some/path/MyApp/└── Flutter/ ├── Debug/ │ ├── Flutter.framework │ ├── App.framework │ ├── FlutterPluginRegistrant.framework │ └── example_plugin.framework (each plugin with iOS platform code is a separate framework) ├── Profile/ │ ├── Flutter.framework │ ├── App.framework │ ├── FlutterPluginRegistrant.framework │ └── example_plugin.framework └── Release/ ├── Flutter.framework ├── App.framework ├── FlutterPluginRegistrant.framework └── example_plugin.framework
在 Xcode 11 中, 你可以添加
--xcframework --no-universal
參數來生成 XCFrameworks,而不是通用 framework。
在 Xcode 中將生成的 frameworks 集成到你的既有應用中。例如,你可以在 some/path/MyApp/Flutter/Release/
目錄拖拽 frameworks 到 你的應用 target 編譯設置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中選擇 "Embed & Sign"。
[圖片上傳失敗...(image-35602-1603942843514)]
在 target 的編譯設置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS
) 增加 $(PROJECT_DIR)/Flutter/Release/
。
[圖片上傳失敗...(image-aa8c6b-1603942843514)]
在 Xcode 項目中即成 frameworks 有很多方法 —— 選擇最適合你的項目的。
你現在可以在 Xcode中使用 ?B
編譯項目。
如果你想在 Debug 編譯配置下使用 Debug 版本的 Flutter frameworks,在 Release 編譯配置下使用 Release 版本的 Flutter frameworks,在
MyApp.xcodeproj/project.pbxproj
文件中,嘗試在所有 Flutter 相關 frameworks 上使用path = "Flutter/$(CONFIGURATION)/example.framework";
替換path = Flutter/Release/example.framework;
(注意添加引號"
)。你也必須在 Framework Search Paths 編譯設置中使用
$(PROJECT_DIR)/Flutter/$(CONFIGURATION)
。
開發
下面我們在既有 iOS 應用中添加單個 Flutter 頁面。
啟動 FlutterEngine 和 FlutterViewController
為了在既有 iOS 應用中展示 Flutter 頁面,請啟動 FlutterEngine
7 和 FlutterViewController
8。
FlutterEngine
充當 Dart VM 和 Flutter 運行時的主機;FlutterViewController
依附于FlutterEngine
,給 Flutter 傳遞 UIKit 的輸入事件,并展示被FlutterEngine
渲染的每一幀畫面。
FlutterEngine
的壽命可能與 FlutterViewController
相同,也可能超過 FlutterViewController
。
通常建議為您的應用預熱一個“長壽”的
FlutterEngine
是因為:? 當展示
FlutterViewController
時,第一幀畫面將會更快展現;? 你的 Flutter 和 Dart 狀態將比一個
FlutterViewController
存活更久;? 在展示 UI 前,你的應用和 plugins 可以與 Flutter 和 Dart 邏輯交互。
加載順序和性能9 里有更多關于預熱 engine 的延遲和內存取舍的分析。
創建一個 FlutterEngine
創建 FlutterEngine
的合適位置取決于您的應用。作為示例,我們將在應用啟動的 app delegate 中創建一個 FlutterEngine
, 并作為屬性暴露給外界。
如果你使用 Objective-C 在 ****AppDelegate.h
:
// AppDelegate.h @import UIKit;@import Flutter; @interface AppDelegate : FlutterAppDelegate // 以下有關于 FlutterAppDelegate 的更多信息@property (nonatomic,strong) FlutterEngine *flutterEngine;@end
在 ****AppDelegate.m
:
// AppDelegate.m #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Used to connect plugins. #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions { self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"]; // 使用默認 Flutter 路由運行默認 Dart 入口 [self.flutterEngine run]; [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; return [super application:application didFinishLaunchingWithOptions:launchOptions];} @end
如果你使用 Swift
在 ****AppDelegate.swift
:
// AppDelegate.swift import UIKitimport Flutterimport FlutterPluginRegistrant // 用于連接 plugins @UIApplicationMainclass AppDelegate: FlutterAppDelegate { // FlutterAppDelegate 有更多信息 lazy var flutterEngine = FlutterEngine(name: "my flutter engine") override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 使用默認 Flutter 路由運行默認 Dart 入口 flutterEngine.run(); GeneratedPluginRegistrant.register(with: self.flutterEngine); return super.application(application, didFinishLaunchingWithOptions: launchOptions); }}
使用 FlutterEngine 展示 FlutterViewController
下面的例子展示了一個普通的 ViewController,包含一個 present FlutterViewController
的按鈕。
如果你使用 Objective-C
// ViewController.m @import Flutter;#import "AppDelegate.h"#import "ViewController.h" @implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; // 制作一個按鈕,當點擊的時候調用 showFlutter 方法 UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; [button addTarget:self action:@selector(showFlutter) forControlEvents:UIControlEventTouchUpInside]; [button setTitle:@"Show Flutter!" forState:UIControlStateNormal]; button.backgroundColor = UIColor.blueColor; button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0); [self.view addSubview:button];} - (void)showFlutter { FlutterEngine *flutterEngine = ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine; FlutterViewController *flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil]; [self presentViewController:flutterViewController animated:YES completion:nil];}@end
如果你使用 Swift
// ViewController.swift import UIKitimport Flutter class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 制作一個按鈕,當點擊的時候調用 showFlutter 方法 let button = UIButton(type:UIButton.ButtonType.custom) button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside) button.setTitle("Show Flutter!", for: UIControl.State.normal) button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0) button.backgroundColor = UIColor.blue self.view.addSubview(button) } @objc func showFlutter() { let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil) }}
現在,你的 iOS 應用中集成了一個 Flutter 頁面。
在上一個例子中,你的默認 Dart 庫的默認入口函數
main()
,將會在AppDelegate
創建FlutterEngine
并調用run
方法時調用。
或者 —— 使用隱式 FlutterEngine 創建 FlutterViewController
上一個示例還有另一個選擇,你可以讓 FlutterViewController
隱式創建它自己的 FlutterEngine
,而不用提前預熱 engine。
不過不建議這樣做,因為按需創建FlutterEngine
的話,在 FlutterViewController
被 present 出來之后,第一幀圖像渲染完之前,將會引入明顯的延遲。但是當 Flutter 頁面很少被展示時,當對決定何時啟動 Dart VM 沒有好的啟發時,當 Flutter 無需在頁面(view controller)之間保持狀態時,此方式可能會有用。
為了不使用已經存在的 FlutterEngine
來展現 FlutterViewController
,省略 FlutterEngine
的創建步驟,并且在創建 FlutterViewController
時,去掉 engine 的引用。
如果你使用 Objective-C
// "ViewController.m // 省略已經存在的代碼- (void)showFlutter { FlutterViewController *flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil]; [self presentViewController:flutterViewController animated:YES completion:nil];}@end
如果你使用 Swift
// ViewController.swift // 省略已經存在的代碼func showFlutter() { let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil)}
查看 加載順序和性能 了解更多關于延遲和內存使用的探索。
使用 FlutterAppDelegate
推薦讓你應用的 UIApplicationDelegate
繼承 FlutterAppDelegate
,但不是必須的。
FlutterAppDelegate
有這些功能:
? 傳遞應用的回調,例如 openURL
, 到 Flutter plugins,例如 local_auth;
? 傳遞狀態欄點擊(這只能在 AppDelegate 中檢測)到 Flutter 的點擊置頂行為。
如果你的 app delegate 不能直接繼承FlutterAppDelegate
,讓你的 app delegate 實現 FlutterAppLifeCycleProvider
協議,來確保 Flutter plugins 接收到必要的回調。否則,依賴這些事件的 plugins 將會有無法預估的行為。
例如:
// AppDelegate.h @import Flutter;@import UIKit;@import FlutterPluginRegistrant; @interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>@property (strong, nonatomic) UIWindow *window;@property (nonatomic,strong) FlutterEngine *flutterEngine;@end
App delegate 的實現中,應該最大化地委托給 FlutterPluginAppLifeCycleDelegate
:
// AppDelegate.m @interface AppDelegate ()@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;@end@implementation AppDelegate- (instancetype)init { if (self = [super init]) { _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; } return self;} - (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions { self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil]; [self.flutterEngine runWithEntrypoint:nil]; [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];} // 返回 key window 的 rootViewController, 如果它是一個 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]; // 傳遞狀態欄的點擊到 key window 上 Flutter 的 rootViewController if (self.rootFlutterViewController != nil) { [self.rootFlutterViewController handleStatusBarTouches:event]; }} - (void)application:(UIApplication*)applicationdidRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings { [_lifeCycleDelegate application:applicationdidRegisterUserNotificationSettings:notificationSettings];} - (void)application:(UIApplication*)applicationdidRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { [_lifeCycleDelegate application:applicationdidRegisterForRemoteNotificationsWithDeviceToken:deviceToken];} - (void)application:(UIApplication*)applicationdidReceiveRemoteNotification:(NSDictionary*)userInfofetchCompletionHandler:(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*)applicationperformActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) { [_lifeCycleDelegate application:application performActionForShortcutItem:shortcutItem completionHandler:completionHandler];} - (void)application:(UIApplication*)applicationhandleEventsForBackgroundURLSession:(nonnull NSString*)identifier completionHandler:(nonnull void (^)(void))completionHandler { [_lifeCycleDelegate application:applicationhandleEventsForBackgroundURLSession:identifier completionHandler:completionHandler];} - (void)application:(UIApplication*)applicationperformFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];} - (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate { [_lifeCycleDelegate addDelegate:delegate];}@end
啟動選項
例子中展示了使用默認啟動選項運行 Flutter。
為了定制化你的 Flutter 運行時,你也可以置頂 Dart 入口、庫和路由。
Dart 入口
在 FlutterEngine
上調用 run
,默認將會調用你的 lib/main.dart
文件里的 main()
函數。
你也可以使用另一個入口方法 runWithEntrypoint
,并使用 NSString
字符串指定一個不同的 Dart 入口。
使用
main()
以外的 Dart 入口函數,必須使用下面的注解,防止被 tree-shaking 優化掉,而沒有編譯。
// main.dart @pragma('vm:entry-point')void myOtherEntrypoint() { ... };
Dart 庫
另外,在指定 Dart 函數時,你可以指定特定文件的特定函數。
下面的例子使用 lib/other_file.dart
文件的 myOtherEntrypoint()
函數取代 lib/main.dart
的 main()
函數:
如果你使用 Objective-C
[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
如果你使用 Swift
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")
路由
當構建 engine 時,可以為你的 Flutter WidgetsApp
設置一個初始路由。
如果你使用 Objective-C
FlutterEngine *flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];[[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute" arguments:@"/onboarding"];[flutterEngine run];
如果你使用 Swift
let flutterEngine = FlutterEngine(name: "my flutter engine")flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/onboarding")flutterEngine.run()
這段代碼使用 "/onboarding"
取代 "/"
,作為你的 dart:ui
的 window.defaultRouteName
請注意:
navigationChannel
上的"setInitialRoute"
必須在啟動FlutterEngine
前調用,才能在 Flutter 的第一幀中顯示期望的路由。特別是,它必須在運行 Dart 入口函數前被調用。入口函數可能會引起一系列的事件,因為runApp
搭建了一個 Material/Cupertino/WidgetsApp,進而隱式創建了一個 Navigator,Navigator 又可能在第一次初始化NavigatorState
時讀取window.defaultRouteName
。運行 engine 后設置初始化路由,將不會有作用.
另外
如果在
FlutterEngine
啟動后,迫切得需要在平臺側改變你當前的 Flutter 路由,可以使用FlutterViewController
里的 pushRoute,或者popRoute。在 Flutter 側推出 iOS 路由,調用
SystemNavigator.pop()
。
查看 路由和導航了解更多 Flutter 路由的內容。
其它
之前的例子僅僅展示了怎樣定制 Flutter 實例初始化的幾種方式,通過 撰寫雙端平臺代碼,你可以在 FlutterViewController
展示 Flutter UI 之前,自由地選擇你喜歡的,推入數據和準備 Flutter 環境的方式。
參考文獻
[1]https://flutter.cn/docs/get-started/install/macos#system-requirements
[2]https://flutter.cn/docs/get-started/install/macos#install-xcode
[3]https://flutter.cn/docs/development/tools/android-studio
[4]https://flutter.cn/docs/development/tools/vs-code
[5]https://flutter.cn/docs/resources/faq#how-big-is-the-flutter-engine
[6]https://flutter.cn/docs/testing/build-modes
[7]https://api.flutter-io.cn/objcdoc/Classes/FlutterEngine.html
[8]https://api.flutter-io.cn/objcdoc/Classes/FlutterViewController.html
[9]https://flutter.cn/docs/development/add-to-app/performance
[10]https://blog.csdn.net/olsq93038o99s/article/details/104177294