本文先介紹一下現(xiàn)有工程如何集成 Flutter 實現(xiàn)混合開發(fā)荒给,以及混合項目如何打包疏虫,再探索下如何降低原生和 Flutter 之間的依賴屏鳍,使 Flutter 開發(fā)對原生開發(fā)的影響盡量降低栏饮,以及一些我在嘗試中遇到的問題及解決吊说。
介紹 Flutter
Flutter 是 Google 發(fā)布的一個用于創(chuàng)建跨平臺嘁信、高性能移動應用的框架。Flutter 和 QT mobile 一樣疏叨,都沒有使用原生控件潘靖,相反都實現(xiàn)了一個自繪引擎,使用自身的布局蚤蔓、繪制系統(tǒng)卦溢。開發(fā)者可以通過 Dart 語言開發(fā) App,一套代碼同時運行在 iOS 和 Android平臺秀又。Flutter 提供了豐富的組件单寂、接口,開發(fā)者可以很快地為 Flutter 添加 Native 擴展吐辙。
前提工作
開發(fā)者需要安裝好 Flutter 的環(huán)境宣决,執(zhí)行flutter doctor -v
驗證。
驗證通過后即可開始集成 Flutter昏苏。
現(xiàn)有原生工程集成 Flutter
最官方的教程應該是Add Flutter to existing apps了尊沸,按照教程如下一步步操作:
1.創(chuàng)建 flutter module
使用flutter create xxx
指令創(chuàng)建的 Flutter 項目包括用于 Flutter/Dart 代碼的非常簡單的工程。你可以修改 main.dart 的內容贤惯,以滿足你的需要洼专,并在此基礎上進行構建。
假設你有一個已經存在 iOS 工程(以 flutterHybridDemo 為例)在some/path/flutterHybridDemo
孵构,那么你新建的 flutter_module 和 iOS 工程應該在同一目錄下(即都在 path 下)屁商。
$ cd some/path/
$ flutter create -t module flutter_module
通過
shift+command+.
顯示/隱藏隱藏文件夾
- lib/main.dart:存放的是 Dart 語言編寫的代碼,這里是核心代碼颈墅;
- pubspec.yaml:配置依賴項的文件蜡镶,比如配置遠程 pub 倉庫的依賴庫,或者指定本地資源(圖片恤筛、字體官还、音頻、視頻等)叹俏;
- .ios/:iOS 部分代碼妻枕;
- .android/:Android 部分代碼僻族;
- build/:存儲 iOS 和 Android 構建文件粘驰;
- test/:測試代碼屡谐。
2.將 flutter module 作為依賴添加到工程
假設文件夾結構如下:
some/path/
flutter_module/
lib/main.dart
.ios/
...
flutterHybridDemo/
flutterHybridDemo.xcodeproj
flutterHybridDemo/
AppDelegate.h
AppDelegate.m
...
集成 Flutter 框架需要使用CocoaPods
,這是因為 Flutter 框架還需要對 flutter_module 中可能包含的任何 Flutter 插件可用蝌数。
- 如果需要愕掏,請參考cocoapods.org了解如何在您的電腦上安裝 CocoaPods。
創(chuàng)建 Podfile:
$ cd some/path/flutterHybridDemo
$ pod init
此時工程中會出現(xiàn)一個 Podfile 文件顶伞,添加項目依賴的第三方庫就在這個文件中配置饵撑,編輯 Podfile 文件添加最后兩行代碼:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'TestOne' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for TestOne
target 'TestOneTests' do
inherit! :search_paths
# Pods for testing
end
target 'TestOneUITests' do
inherit! :search_paths
# Pods for testing
end
end
#新添加的代碼
flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
- 如果你的工程(flutterHybridDemo)已經在使用 Cocoapods ,你只需要做以下幾件事來整合你的 flutter_module 應用程序:
(1)添加如下內容到 Podfile:
flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
(2)執(zhí)行pod install
當你在some/path/flutter_module/pubspec.yaml
中修改 Flutter 插件依賴時唆貌,需要先執(zhí)行flutter packages get
通過 podhelper.rb 腳本來刷新插件列表滑潘,然后再從some/path/flutterHybridDemo
執(zhí)行一次pod install
。
podhelper.rb 腳本將確保你的插件和 Flutter 框架被添加到你的工程中锨咙,以及 bitcode 被禁用语卤。
(3)禁用 bitcode
因為 Flutter 現(xiàn)在不支持 bitcode。需要設置 Build Settings->Build Options->Enable Bitcode 為 NO酪刀。
3.為編譯 Dart 代碼配置 build phase
打開 iOS 工程粹舵,選中項目的 Build Phases 選項,點擊左上角+號按鈕骂倘,選擇 New Run Script Phase眼滤。
將下面的 shell 腳本添加到輸入框中:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
最后,確保 Run Script 這一行在 "Target dependencies" 或者 "Check Pods Manifest.lock" 后面历涝。
至此诅需,你可以編譯一下工程確保無誤:?B
。
4.在 iOS 工程中使用 FlutterViewController
首先聲明你的 AppDelegate 是 FlutterAppDelegate 的子類荧库。然后定義一個 FlutterEngine 屬性诱担,它可以幫助你注冊一個沒有 FlutterViewController 實例的插件。
在 AppDelegate.h:
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
在AppDelegate.m电爹,修改didFinishLaunchingWithOptions
方法如下:
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
#include "AppDelegate.h"
@implementation AppDelegate
// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
如果 AppDelegate 已經繼承于別的類的時候蔫仙,可以通過讓你的 delegate 實現(xiàn)FlutterAppLifeCycleProvider
協(xié)議:
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@end
然后生命周期方法應該由 FlutterPluginAppLifeCycleDelegate 來代理:
@implementation AppDelegate
{
FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}
- (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self]; // Only if you are using Flutter plugins.
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
在 ViewController 中添加跳轉到 FlutterViewController 的測試代碼即可:
#import "ViewController.h"
#import <Flutter/Flutter.h>
#import "AppDelegate.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self
action:@selector(handleButtonAction)
forControlEvents:UIControlEventTouchUpInside];
[button setTitle:@"Jump to flutterViewController" forState:UIControlStateNormal];
[button setBackgroundColor:[UIColor grayColor]];
button.frame = CGRectMake(80.0, 210.0, 300.0, 40.0);
button.center = self.view.center;
[self.view addSubview:button];
}
- (void)handleButtonAction {
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
FlutterEngine *flutterEngine = delegate.flutterEngine;
FlutterViewController *flutterVC = [[FlutterViewController alloc]initWithEngine:flutterEngine nibName:nil bundle:nil];
[self presentViewController:flutterVC animated:YES completion:nil];
}
@end
5.使用熱重載的方式調試 Dart 代碼
熱重載指的是不用重新啟動就看到修改后的效果,類似 web 網頁開發(fā)時保存就看到效果的方式丐箩。
進入 flutter module摇邦,在終端執(zhí)行命令:
$ cd some/path/flutter_module
$ flutter run
并且你能在控制臺中看下如下內容:
?? To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:54741/
For a more detailed help message, press "h". To quit, press "q".
你可以在 flutter_module 中編輯 Dart code,然后在終端輸入 r 來使用熱重載屎勘。你也可以在瀏覽器中輸入上面的 URL 來查看斷點施籍、分析內存和其他的調試任務。
集成 Flutter 后工程打包
1. flutter build ios
執(zhí)行flutter build ios
以創(chuàng)建 release 版本(flutter build 默認為--release概漱,如需創(chuàng)建 debug 版本執(zhí)行flutter build ios —debug
)丑慎。
2.成功后修改 Xcode 為 release 模式配置
3.最后選擇 Product > Archive 以生成構建版本即可
混合工程改造優(yōu)化
Flutter 的工程結構比較特殊,由 Flutter 目錄、Native 工程的目錄(即 iOS 和 Android 兩個目錄)組成竿裂。默認情況下玉吁,引入了 Flutter 的 Native 工程無法脫離父目錄進行獨立構建和運行,因為它會反向依賴于 Flutter 相關的庫和資源腻异。
實際上进副,在真實的開發(fā)情況下,開發(fā)者很少會創(chuàng)建一個完全 Flutter 的工程重寫項目悔常,更多的情況是原生工程集成 Flutter影斑。
1.問題
這樣就帶來了一系列問題:
(1)構建打包問題:引入 Flutter 后,Native 工程因對其有了依賴和耦合机打,從而無法獨立編譯構建矫户。在 Flutter 環(huán)境下,工程的構建是從 Flutter 的構建命令開始残邀,執(zhí)行過程中包含了 Native 工程的構建吏垮,開發(fā)者要配置完整的 Flutter 運行環(huán)境才能走通整個流程;
(2)混合編譯帶來的開發(fā)效率的降低:在轉型 Flutter 的過程中必然有許多業(yè)務仍使用 Native 進行開發(fā)罐旗,工程結構的改動會使開發(fā)無法在純 Native 環(huán)境下進行膳汪,而適配到 Flutter 工程結構對純 Native 開發(fā)來說又會造成不必要的構建步驟,造成開發(fā)效率的降低九秀。
2.目標
希望能將 Flutter 依賴抽取出來遗嗽,作為一個 Flutter 依賴庫,供純 Native 工程引用鼓蜒,無需配置完整的 Flutter 環(huán)境痹换。
3.Flutter 產物
iOS 工程對 Flutter 有如下依賴:
Flutter.framework:Flutter 庫和引擎
App.framework:dart 業(yè)務源碼相關文件
flutter_assets:Flutter依賴的靜態(tài)資源,如字體都弹,圖片等
Flutter Plugin:編譯出來的各種 plugin 的 framework
把以上依賴的編譯結果抽取出來娇豫,即是 Flutter 相關代碼的最終產物。
那么我們只需要將這些打包成一個 SDK 依賴的形式提供給 Native 工程畅厢,就可以解除 Native 工程對 Flutter 工程的直接依賴冯痢。
產物的產生:
對 flutter 工程執(zhí)行 flutter build 命令后,生成在.ios/Flutter
目錄下框杜,直接手動拷貝 framework 到主工程即可浦楣。
注意事項:
framework 選擇 Create groups 加入文件夾,flutter_assets 選擇 Create folder references 加入文件夾咪辱。
加入完成后的結構:
framework 加入后振劳,記住一定要確認 framework 已在 TARGETS -> General -> Embedded Binaries 中添加完成。
最后改造 APPDelegate 即可:
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate>
@property (strong, nonatomic) FlutterEngine *flutterEngine;
@end
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc]initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
return YES;
}
4. 優(yōu)化
為了更方便管理 framework油狂,可以將這些文件上傳到遠程倉庫历恐,通過 CocoaPods 導入寸癌,Native 項目只需及時更新 pod 依賴即可。
我遇到過的一些問題及解決
1.在 Android Studio 上跑設備
More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.
提示你當前有兩個模擬器設備弱贼,跑設備的時候要選擇運行在哪個設備上蒸苇,flutter run
后面拼接上“-d <deviceId>”,deviceId 是第二列的內容哮洽。
flutter run -d emulator-5554
flutter run -d C517D2D4-EAFA-42CA-B260-A18FA0ABFF60
電腦連著真機也同理填渠,改成真機的 deviceId 即可弦聂。
2.flutter build ios 報錯
build 時可能遇到的錯誤:
It appears that your application still contains the default signing identifier.Try replacing 'com.example' with your signing id in Xcode:
open ios/Runner.xcworkspace
解決方法:
修改some/flutter_module/.ios/
下 Runner 工程的 Bundle Identifier 和原生工程的一致鸟辅,再次運行flutter build ios
即可。
3.開發(fā)時打包產物編譯失敗
當你用flutter build ios
的產物添加到原生工程中莺葫,跳轉到 Flutter 界面會黑屏并報出如下錯誤:
Failed to find snapshot: …/Library/Developer/CoreSimulator/Devices/…/data/Containers/Bundle/Application/…/FlutterMixDemo.app/Frameworks/App.framework/flutter_assets/kernel_blob.bin
如何解決:
調試模式下用flutter build ios —debug
的產物匪凉,再次拖入工程即可。
原因:
首先我們對比下捺檬,執(zhí)行flutter build ios
和執(zhí)行flutter build ios --debug
后 .ios/Flutter/App.framework/flutter_assets
的文件內容:
可以發(fā)現(xiàn)再层,差別是在于三個文件:isolate_snapshot_data、kernel_blob.bin堡纬、vm_snapshot_data聂受。
這里涉及 Flutter 的編譯模式知識,具體可以參閱Flutter 的兩種編譯模式烤镐。
Flutter 開發(fā)階段的編譯模式:使用了 Kernel Snapshot 模式編譯蛋济,打包產物中,可以發(fā)現(xiàn)幾樣東西:
isolate_snapshot_data:用于加速 isolate 啟動炮叶,業(yè)務無關代碼碗旅,固定,僅和 flutter engine 版本有關镜悉;
platform.dill:和 Dart VM 相關的 kernel 代碼祟辟,僅和 Dart 版本以及 engine 編譯版本有關。固定侣肄,業(yè)務無關代碼旧困;
vm_snapshot_data:用于加速 Dart VM 啟動的產物,業(yè)務無關代碼稼锅,僅和 flutter engine 版本有關叮喳;
kernel_blob.bin:業(yè)務代碼產物 。
Flutter 生產階段的編譯模式:選擇了 AOT 打包缰贝。
4.集成后 Native 工程報錯
Shell Script Invocation Error
line 2:/packages/flutter_tools/bin/xcode_backend.sh: No such file or directory
解決方法:
修改 TARGETS -> Build Setting -> FLUTTER_ROOT 為電腦安裝的 Flutter 環(huán)境的路徑即可馍悟。
5.如何在 iOS 工程 Debug 模式下使用 release 模式的 flutter
只需要將 Generated.xcconfig 中的 FLUTTER_BUILD_MODE 修改為 release,F(xiàn)LUTTER_FRAMEWORK_DIR 修改為 release 對應的路徑即可剩晴。
其他
1.說明:
本文僅供用于學習參考锣咒,請勿用于商業(yè)用途侵状。如需轉載,請標明出處毅整,謝謝合作趣兄。
本文系參考網絡公開 Flutter 學習資料以及個人學習體會總結所得,部分內容為網絡公開學習資料悼嫉,如有侵權請聯(lián)系作者刪除艇潭。
2.參考資料:
Flutter 中文網:https://flutterchina.club
咸魚技術-flutter:https://www.yuque.com/xytech/flutter
iOS Native混編Flutter交互實踐:https://juejin.im/post/5bb033515188255c5e66f500#heading-3
Flutter混編之路——開發(fā)集成(iOS篇):http://www.reibang.com/p/48a9083ebe89
作者簡介
就職于甜橙金融(翼支付)信息技術部,負責 iOS 客戶端開發(fā)戏蔑。