iOS端Flutter混合工程及互動實踐
[TOC]
混合工程搭建
為了專案可以支援Flutter和Native混合開發的模式,我們需要在對原生專案無侵入的條件下接入flutter,原生專案直接依賴flutter專案產物,如下圖所示:
Flutter官方文件提供的混合方案
1.建立Flutter工程
安裝flutter,自行百度;任意目錄下執行flutter create -t module my_flutter
,"my_flutter"
是要建立的 Flutter 工程的名稱。
2.通過 Cocoapods 將 Flutter 引入 現有 Native 工程
在Podfile
新增以下下程式碼
flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path,'.ios','Flutter','podhelper.rb')),binding)
複製程式碼
然後執行pod install
這個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 這個 pod hooks 來關閉 Native 工程的 bitcode,並將 'Generated.xcconfig' 檔案加入 Native 工程。
#####3.修改 Native 工程
開啟Xcode工程,選擇要加入 Flutter App 的 target,選擇
Build Phases
,點選頂部的 + 號,選擇New Run Script Phase
,然後輸入以下指令碼
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
複製程式碼
這裡執行的flutter包根目錄shell指令碼的作用:
- build: 根據當前 Xcode 工程的 'configuration' 和其他編譯配置編譯 Flutter 工程
- embed: 將 build 出來的 framework、資源包放入 Xcode 編譯目錄,並簽名 framework
這裡就有了一個問題,Flutter 工程依賴 Native工程來執行編譯,影響Native工程的開發流程與打包流程,開發Native的人也需要安裝Flutter環境才能除錯APP
4.總結
以上操作可以簡單的理解為,Native工程配置好指令碼後,執行時會先編譯Flutter專案,Flutter專案會在自己的相應目錄生成Flutter.framework、依賴的Native外掛等產物,最終在pod中配置好路徑等引數,通過pod本地依賴的方式集成了flutter。
實現無侵入Native Flutter 混合工程
基於官方的方案,為了實現這個目標,需要實現以下2點:
- Flutter 工程裡建立一個打包指令碼,可以產生 Flutter 工程產物並上傳到遠端倉庫;
- 在 Native 工程用pod依賴遠端倉庫中的Flutter工程產物;並且保留依賴本地Flutter工程原始碼的功能,便於除錯。
1.Flutter專案打包指令碼
在專案目錄中加入build_ios.sh檔案,指令碼自動打包 Flutter 工程大致分為一下幾個步驟:
-
flutter_get_packages()
:檢查 Flutter 環境,拉取 Flutter plugin -
build_flutter_app()
:編譯 Flutter 工程得到產物並copy到特定檔案路徑下,主要邏輯和官方提供的xcode_backend.sh
指令碼差不多 -
flutter_copy_packages()
:得到 Flutter 產物中的 Native 外掛,並copy到特定檔案路徑下 -
upload_product()
:release模式中將產物同步上傳到git中
執行./build_ios.h -m debug
./build_ios.h -m release
得到不同環境的產物,並上傳遠端倉庫
2.Native 依賴 Flutter 產物
這部分我們需要實現獲取 Flutter 工程 release 產物,並整合到 Native 專案,並保留可以依賴本地 Flutter 工程的能力。
在原生專案中加入flutterhelper.rb
指令碼,分為如下幾個步驟:
- 獲取 Flutter 工程產物
- 獲取 release 產物
install_release_flutter_app
:clone遠端倉庫中的Flutter產物到本地 - 獲取 debug 產物
install_debug_flutter_app
:在 Flutter工程路徑下,執行 build_ios.sh -m debug 進行打包,然後得到 debug 產物目錄
- 獲取 release 產物
- 通過 pod 引入 Flutter 工程產物
install_release_flutter_app_pod
:遍歷Flutter產物目錄,使用pod sub,:path=>sub_abs_path
依賴Flutter.FrameWork、Native外掛等
podfile
中配置如下:
# 為true時,debug環境 為false時,release環境
FLUTTER_DEBUG_APP=true
# 如果指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_URL= "http://appinstall.aiyoumi.com:8282/flutter/iOS_flutter_product.git"
# flutter git 分支,預設為master
# 如果指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_BRANCH="master"
# flutter本地工程目錄,絕對路徑或者相對路徑,如果有值則git相關的配置無效
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
複製程式碼
與原生互動實踐
Flutter官方混合方案
1.Flutter呼叫原生
Flutter提供了FlutterMethodChannel實現了Flutter呼叫原生方法的功能,如下:
//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的內容");
}
}
}];
//flutter中
// 建立一個給native的channel
static const methodChannel = const MethodChannel('com.pages.your/native_get');
_iOSPushToVC() async {
dynamic result;
result = await methodChannel.invokeMethod('iOSFlutter','引數');
}
複製程式碼
2.原生呼叫Flutter
Flutter提供了FlutterEventChannel來完成原生呼叫Flutter
// native中
FlutterEventChannel *evenChannal = [FlutterEventChannel eventChannelWithName:channelName binaryMessenger:flutterViewController];
// 代理FlutterStreamHandler
[evenChannal setStreamHandler:self];
#pragma mark - <FlutterStreamHandler>
// 這個onListen是Flutter端開始監聽這個channel時的回撥,第二個引數 EventSink是用來傳資料的載體。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(FlutterEventSink)events {
// arguments flutter給native的引數
if (events) {
events(@"push傳值給flutter的vc");
}
return nil;
}
// flutter中
// 註冊一個通知
static const EventChannel eventChannel = const EventChannel('com.pages.your/native_post');
// 監聽事件,同時傳送引數
eventChannel.receiveBroadcastStream(12345).listen(_onEvent,onError: _onError);
String naviTitle = 'title' ;
// 回撥事件
void _onEvent(Object event) {
setState(() {
naviTitle = event.toString();
});
}
複製程式碼
3.總結
以上就是官方提供的混合開發方案了,這個方案有一個巨大的缺點,就是在原生和Flutter頁面疊加跳轉時記憶體不斷增大,因為FlutterView和FlutterViewController每次跳轉都會新建一個物件,建立的Flutter頁面越多記憶體就會暴增,尤其是在iOS上還有記憶體洩露的問題。
flutter_boost混合方案
1.簡介
我們可以這樣簡單去理解這個方案:我們把共享的Flutter View
當成一個畫布,然後用一個Native
的容器作為邏輯的頁面。每次在開啟一個容器的時候我們通過通訊機制通知Flutter View
繪製成當前的邏輯頁面,然後將Flutter View放到當前容器裡面。
頁面棧完全由原生控制,每一個flutter
頁面對應一個原生容器(ViewController
和Activity
),原生端建立FlutterRouter
實現FLBPlatform
中的介面,flutter和原生的相互呼叫都會執行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跳轉原生頁面直接呼叫原生專案中的路由
// flutter中:
// 傳遞協議名和頁面所需初始化引數
ACRouter.openUrl("mizlicai://product/normalProductDetail",{'serial': 'PI_11221'},routeCallback: (Map<dynamic,dynamic> result) {
// 處理回撥結果
print("did recieve second route result $result");
});
// Native中:
// TODO:普通產品詳情
[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':{},dynamic> result) {
// 處理回撥結果
print("did recieve second route result $result");
});
複製程式碼
#####2.協議支援 flutter可以呼叫原生專案元件化的路由協議(米莊iOS路由協議),來跳轉原生頁面、呼叫原生介面等。 #####3.網路資料請求 為了保持和原生請求框架保持同一份邏輯,使用抽象類的方式封裝請求工具,Flutter啟動時判斷環境,使用真實請求類還是Mock請求類。
// main.dart
if (ApiClient.isProduction) {
ApiClient.request = RealRequest();
} else {
ApiClient.request = MockRequest();
}
複製程式碼
MockRequest和RealRequest分別實現父類send方法,RealRequest通過ACRouter呼叫原生髮起網路請求,MockRequest解析本地json
// 發起請求
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,dynamic> result) {
callback(result);
});
}
// MockRequest
void send(String url,Function callback) {
dynamic responseJson =
MockRequest.mock(action: getJsonName(url),param: param);
callback(responseJson);
}
複製程式碼
4.頁面導航
Flutter頁面棧由原生控制,使用自己的導航欄。關閉不同頁面的方法
// 關閉返回上一頁
static Future<bool> closeCurPage()
// 返回到特定頁面,使用openUrl互動
ACRouter.openUrl('mizlicai://product/closeToRoot',dynamic> result) {
callback(result);
});
複製程式碼
5.原生接入
在Podfile
中新增配置,可以切換本地,遠端,debug等環境
platform :ios,'9.0'
# 為true時,debug環境 為false時,release環境
FLUTTER_DEBUG_APP=false
# 如果指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_URL= "http://appinstall.aiyoumi.com:8282/flutter/iOS_flutter_product.git"
# flutter git 分支,預設為master
# 如果指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_BRANCH="master"
# flutter本地工程目錄,絕對路徑或者相對路徑,如果有值則git相關的配置無效
FLUTTER_APP_PATH="/Users/zouyongfeng/ac_flutter_module"
eval(File.read(File.join(__dir__,binding)
複製程式碼
AppDelegate中,初始化flutterboost
,傳入FlutterRouter
#import "FlutterRouter.h"
- (void)startFlutter {
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:[FlutterRouter sharedRouter]
onStart:^(FlutterViewController *fvc) {
}];
}
複製程式碼