[TOC]
混合工程搭建
為了項(xiàng)目可以支持Flutter和Native混合開發(fā)的模式,我們需要在對原生項(xiàng)目無侵入的條件下接入flutter止剖,原生項(xiàng)目直接依賴flutter項(xiàng)目產(chǎn)物亮靴,如下圖所示:
Flutter官方文檔提供的混合方案
1.創(chuàng)建Flutter工程
安裝flutter馍盟,自行百度;任意目錄下執(zhí)行flutter create -t module my_flutter
台猴,"my_flutter"
是要?jiǎng)?chuàng)建的 Flutter 工程的名稱朽合。
2.通過 Cocoapods 將 Flutter 引入 現(xiàn)有 Native 工程
在Podfile
添加以下下代碼
flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
然后執(zhí)行pod install
這個(gè)ruby腳本主要做下面4件事情:
- 解析 'Generated.xcconfig' 文件,獲取 Flutter 工程配置信息饱狂,文件在'my_flutter/.ios/Flutter/'目錄下曹步,文件中包含了 Flutter SDK 路徑、Flutter 工程路徑休讳、Flutter 工程入口讲婚、編譯目錄等。
- 將 Flutter SDK 中的 Flutter.framework 通過 pod 添加到 Native 工程俊柔。
- 將 Flutter 工程依賴的Native插件通過 pod 添加到 Native 工程
- 使用 post_install 這個(gè) pod hooks 來關(guān)閉 Native 工程的 bitcode筹麸,并將 'Generated.xcconfig' 文件加入 Native 工程。
3.修改 Native 工程
打開Xcode工程雏婶,選擇要加入 Flutter App 的 target物赶,選擇 Build Phases
,點(diǎn)擊頂部的 + 號留晚,選擇 New Run Script Phase
酵紫,然后輸入以下腳本
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
這里執(zhí)行的flutter包根目錄shell腳本的作用:
- build: 根據(jù)當(dāng)前 Xcode 工程的 'configuration' 和其他編譯配置編譯 Flutter 工程
- embed: 將 build 出來的 framework、資源包放入 Xcode 編譯目錄错维,并簽名 framework
這里就有了一個(gè)問題奖地,F(xiàn)lutter 工程依賴 Native工程來執(zhí)行編譯,影響Native工程的開發(fā)流程與打包流程赋焕,開發(fā)Native的人也需要安裝Flutter環(huán)境才能調(diào)試APP
4.總結(jié)
以上操作可以簡單的理解為参歹,Native工程配置好腳本后,運(yùn)行時(shí)會(huì)先編譯Flutter項(xiàng)目隆判,F(xiàn)lutter項(xiàng)目會(huì)在自己的相應(yīng)目錄生成Flutter.framework犬庇、依賴的Native插件等產(chǎn)物僧界,最終在pod中配置好路徑等參數(shù),通過pod本地依賴的方式集成了flutter臭挽。
實(shí)現(xiàn)無侵入Native Flutter 混合工程
基于官方的方案捎泻,為了實(shí)現(xiàn)這個(gè)目標(biāo),需要實(shí)現(xiàn)以下2點(diǎn):
- Flutter 工程里創(chuàng)建一個(gè)打包腳本埋哟,可以產(chǎn)生 Flutter 工程產(chǎn)物并上傳到遠(yuǎn)程倉庫笆豁;
- 在 Native 工程用pod依賴遠(yuǎn)程倉庫中的Flutter工程產(chǎn)物;并且保留依賴本地Flutter工程源碼的功能赤赊,便于調(diào)試闯狱。
1.Flutter項(xiàng)目打包腳本
在項(xiàng)目目錄中加入build_ios.sh文件,腳本自動(dòng)打包 Flutter 工程大致分為一下幾個(gè)步驟:
-
flutter_get_packages()
:檢查 Flutter 環(huán)境抛计,拉取 Flutter plugin -
build_flutter_app()
:編譯 Flutter 工程得到產(chǎn)物并copy到特定文件路徑下哄孤,主要邏輯和官方提供的xcode_backend.sh
腳本差不多 -
flutter_copy_packages()
:得到 Flutter 產(chǎn)物中的 Native 插件,并copy到特定文件路徑下 -
upload_product()
:release模式中將產(chǎn)物同步上傳到git中
執(zhí)行./build_ios.h -m debug
./build_ios.h -m release
得到不同環(huán)境的產(chǎn)物吹截,并上傳遠(yuǎn)程倉庫
2.Native 依賴 Flutter 產(chǎn)物
這部分我們需要實(shí)現(xiàn)獲取 Flutter 工程 release 產(chǎn)物瘦陈,并集成到 Native 項(xiàng)目,并保留可以依賴本地 Flutter 工程的能力波俄。
在原生項(xiàng)目中加入flutterhelper.rb
腳本晨逝,分為如下幾個(gè)步驟:
- 獲取 Flutter 工程產(chǎn)物
- 獲取 release 產(chǎn)物
install_release_flutter_app
:clone遠(yuǎn)程倉庫中的Flutter產(chǎn)物到本地 - 獲取 debug 產(chǎn)物
install_debug_flutter_app
:在 Flutter工程路徑下,執(zhí)行 build_ios.sh -m debug 進(jìn)行打包懦铺,然后得到 debug 產(chǎn)物目錄
- 獲取 release 產(chǎn)物
- 通過 pod 引入 Flutter 工程產(chǎn)物
install_release_flutter_app_pod
:遍歷Flutter產(chǎn)物目錄捉貌,使用pod sub, :path=>sub_abs_path
依賴Flutter.FrameWork、Native插件等
podfile
中配置如下:
# 為true時(shí)冬念,debug環(huán)境 為false時(shí)趁窃,release環(huán)境
FLUTTER_DEBUG_APP=true
# 如果指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_URL= "http://appinstall.aiyoumi.com:8282/flutter/iOS_flutter_product.git"
# flutter git 分支急前,默認(rèn)為master
# 如果指定了FLUTTER_APP_PATH醒陆,則此配置失效
FLUTTER_APP_BRANCH="master"
# flutter本地工程目錄,絕對路徑或者相對路徑裆针,如果有值則git相關(guān)的配置無效
FLUTTER_APP_PATH="/Users/zouyongfeng/ac_flutter_module"
eval(File.read(File.join(__dir__, 'flutterhelper.rb')), binding)
最后在jenkins中配置好打包job即可刨摩,如下:
cd ${WORKSPACE}
if [[ ! -d "${FLUTTER_PROJECT_Name}" ]]; then
git clone ${FLUTTER_PROJECT_GIT_REPO} ${FLUTTER_PROJECT_Name} -b ${PROJECT_GIT_BRANCH}
fi
if [[ ! -d "${FLUTTER_PRODUCT_Name}" ]]; then
git clone ${FLUTTER_PRODUCT_GIT_REPO} ${FLUTTER_PRODUCT_Name} -b ${PROJECT_GIT_BRANCH}
fi
cd ${WORKSPACE}/${FLUTTER_PRODUCT_Name}
git fetch
git reset --hard
git checkout ${PROJECT_GIT_BRANCH}
git pull --no-commit --all
cd ${WORKSPACE}/${FLUTTER_PROJECT_Name}
git fetch
git reset --hard
git checkout ${PROJECT_GIT_BRANCH}
git pull --no-commit --all
source ~/.bash_profile
sh build_ios.sh -m release
與原生交互實(shí)踐
Flutter官方混合方案
1.Flutter調(diào)用原生
Flutter提供了FlutterMethodChannel實(shí)現(xiàn)了Flutter調(diào)用原生方法的功能,如下:
//native中
FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
[flutterViewController setInitialRoute:@"myApp"];
__weak __typeof(self) weakSelf = self;
// 要與main.dart中一致
NSString *channelName = @"com.pages.your/native_get";
FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterViewController];
[messageChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"iOSFlutter"]) {
TargetViewController *vc = [[TargetViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
if (result) {
result(@"返回給flutter的內(nèi)容");
}
}
}];
//flutter中
// 創(chuàng)建一個(gè)給native的channel
static const methodChannel = const MethodChannel('com.pages.your/native_get');
_iOSPushToVC() async {
dynamic result;
result = await methodChannel.invokeMethod('iOSFlutter', '參數(shù)');
}
2.原生調(diào)用Flutter
Flutter提供了FlutterEventChannel來完成原生調(diào)用Flutter
// native中
FlutterEventChannel *evenChannal = [FlutterEventChannel eventChannelWithName:channelName binaryMessenger:flutterViewController];
// 代理FlutterStreamHandler
[evenChannal setStreamHandler:self];
#pragma mark - <FlutterStreamHandler>
// 這個(gè)onListen是Flutter端開始監(jiān)聽這個(gè)channel時(shí)的回調(diào)据块,第二個(gè)參數(shù) EventSink是用來傳數(shù)據(jù)的載體码邻。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(FlutterEventSink)events {
// arguments flutter給native的參數(shù)
if (events) {
events(@"push傳值給flutter的vc");
}
return nil;
}
// flutter中
// 注冊一個(gè)通知
static const EventChannel eventChannel = const EventChannel('com.pages.your/native_post');
// 監(jiān)聽事件折剃,同時(shí)發(fā)送參數(shù)
eventChannel.receiveBroadcastStream(12345).listen(_onEvent,onError: _onError);
String naviTitle = 'title' ;
// 回調(diào)事件
void _onEvent(Object event) {
setState(() {
naviTitle = event.toString();
});
}
3.總結(jié)
以上就是官方提供的混合開發(fā)方案了另假,這個(gè)方案有一個(gè)巨大的缺點(diǎn),就是在原生和Flutter頁面疊加跳轉(zhuǎn)時(shí)內(nèi)存不斷增大怕犁,因?yàn)镕lutterView和FlutterViewController每次跳轉(zhuǎn)都會(huì)新建一個(gè)對象边篮,創(chuàng)建的Flutter頁面越多內(nèi)存就會(huì)暴增己莺,尤其是在iOS上還有內(nèi)存泄露的問題。
flutter_boost混合方案
1.簡介
我們可以這樣簡單去理解這個(gè)方案:我們把共享的Flutter View
當(dāng)成一個(gè)畫布戈轿,然后用一個(gè)Native
的容器作為邏輯的頁面凌受。每次在打開一個(gè)容器的時(shí)候我們通過通信機(jī)制通知Flutter View
繪制成當(dāng)前的邏輯頁面,然后將Flutter View放到當(dāng)前容器里面思杯。
頁面棧完全由原生控制胜蛉,每一個(gè)flutter
頁面對應(yīng)一個(gè)原生容器(ViewController
和Activity
),原生端創(chuàng)建FlutterRouter
實(shí)現(xiàn)FLBPlatform
中的接口色乾,flutter和原生的相互調(diào)用都會(huì)執(zhí)行FlutterRouter
中的openPage
接口誊册。代碼如下:
// iOS: FlutterRouter
- (void)openPage:(NSString *)name params:(NSDictionary *)params animated:(BOOL)animated completion:(void (^)(BOOL finished))completion {
[ACRouter openWithURLString:name userInfo:params completion:^(ACRouterOutModel * _Nonnull outModel) {
[FlutterBoostPlugin.sharedInstance onResultForKey:[params objectForKey:requestIdKey] resultData:outModel.data params:@{}];
if(completion) completion(YES);
}];
}
flutter端建立ACRouter
封裝flutterboost
,flutter跳轉(zhuǎn)原生頁面直接調(diào)用原生項(xiàng)目中的路由
// flutter中:
// 傳遞協(xié)議名和頁面所需初始化參數(shù)
ACRouter.openUrl("mizlicai://product/normalProductDetail", {'serial': 'PI_11221'},
routeCallback: (Map<dynamic, dynamic> result) {
// 處理回調(diào)結(jié)果
print("did recieve second route result $result");
});
// Native中:
// TODO:普通產(chǎn)品詳情
[ACRouter registerWithURLString:@"mizlicai://product/normalProductDetail" handler:^(NSDictionary * _Nullable paramsIn) {
ProductDetailViewController *vc = [[ProductDetailViewController alloc] init];
vc.serial = [paramsIn valueForKey:@"serial"];
vc.origin = [paramsIn valueForKey:@"origin"];
[[UIViewController mz_topController].navigationController pushViewController:vc animated:YES];
}];
flutter端和原生打開flutter頁面
// 原生中
[ACRouter registerWithURLString:@"mizlicai://flutter/open" handler:^(NSDictionary * _Nullable paramsIn) {
NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithDictionary:paramsIn[@"params"]];
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:paramsIn[@"pageName"] params:params];
[[UIViewController mz_topController].navigationController pushViewController:vc animated:animated];
ACRouterCompletionBlock action = paramsIn[ACRouterParameterCompletion];
if (action) {
ACRouterOutModel *outModel = [[ACRouterOutModel alloc] init];
action(outModel);
}
}];
//flutter中
ACRouter.openUrl("mizlicai://flutter/open", {'pageName': 'userCenter','params':{},
routeCallback: (Map<dynamic, dynamic> result) {
// 處理回調(diào)結(jié)果
print("did recieve second route result $result");
});
2.協(xié)議支持
flutter可以調(diào)用原生項(xiàng)目組件化的路由協(xié)議(米莊iOS路由協(xié)議)暖璧,來跳轉(zhuǎn)原生頁面案怯、調(diào)用原生接口等。
3.網(wǎng)絡(luò)數(shù)據(jù)請求
為了保持和原生請求框架保持同一份邏輯澎办,使用抽象類的方式封裝請求工具嘲碱,F(xiàn)lutter啟動(dòng)時(shí)判斷環(huán)境,使用真實(shí)請求類還是Mock請求類局蚀。
// main.dart
if (ApiClient.isProduction) {
ApiClient.request = RealRequest();
} else {
ApiClient.request = MockRequest();
}
MockRequest和RealRequest分別實(shí)現(xiàn)父類send方法麦锯,RealRequest通過ACRouter調(diào)用原生發(fā)起網(wǎng)絡(luò)請求,MockRequest解析本地json
// 發(fā)起請求
ApiClient.request.send(Api.userCenter, HttpRequest.GET, {},
(Map response) {
});
// RealRequest
void send(String url, String requestType, Map param, Function callback) {
param.addAll({'url': url, 'requestType': requestType});
ACRouter.openUrl(RouteCst.httpFlutterRequest, param,
routeCallback: (Map<dynamic, dynamic> result) {
callback(result);
});
}
// MockRequest
void send(String url, String requestType, Map param, Function callback) {
dynamic responseJson =
MockRequest.mock(action: getJsonName(url), param: param);
callback(responseJson);
}
4.頁面導(dǎo)航
Flutter頁面棧由原生控制琅绅,使用自己的導(dǎo)航欄离咐。關(guān)閉不同頁面的方法
// 關(guān)閉返回上一頁
static Future<bool> closeCurPage()
// 返回到特定頁面,使用openUrl交互
ACRouter.openUrl('mizlicai://product/closeToRoot', param,
routeCallback: (Map<dynamic, dynamic> result) {
callback(result);
});
5.原生接入
在Podfile
中添加配置奉件,可以切換本地宵蛀,遠(yuǎn)程,debug等環(huán)境
platform :ios, '9.0'
# 為true時(shí)县貌,debug環(huán)境 為false時(shí)术陶,release環(huán)境
FLUTTER_DEBUG_APP=false
# 如果指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_URL= "http://appinstall.aiyoumi.com:8282/flutter/iOS_flutter_product.git"
# flutter git 分支煤痕,默認(rèn)為master
# 如果指定了FLUTTER_APP_PATH梧宫,則此配置失效
FLUTTER_APP_BRANCH="master"
# flutter本地工程目錄,絕對路徑或者相對路徑摆碉,如果有值則git相關(guān)的配置無效
FLUTTER_APP_PATH="/Users/zouyongfeng/ac_flutter_module"
eval(File.read(File.join(__dir__, 'flutterhelper.rb')), binding)
AppDelegate中塘匣,初始化flutterboost
,傳入FlutterRouter
#import "FlutterRouter.h"
- (void)startFlutter {
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:[FlutterRouter sharedRouter]
onStart:^(FlutterViewController *fvc) {
}];
}