iOS端Flutter混合工程及交互實(shí)踐

[TOC]

混合工程搭建

為了項(xiàng)目可以支持Flutter和Native混合開發(fā)的模式,我們需要在對原生項(xiàng)目無侵入的條件下接入flutter止剖,原生項(xiàng)目直接依賴flutter項(xiàng)目產(chǎn)物亮靴,如下圖所示:


TB1OqY3Ff1TBuNjy0FjXXajyXXa-1279-1125.png

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):

  1. Flutter 工程里創(chuàng)建一個(gè)打包腳本埋哟,可以產(chǎn)生 Flutter 工程產(chǎn)物并上傳到遠(yuǎn)程倉庫笆豁;
  2. 在 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)物目錄
  • 通過 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.簡介
1552968436255-e781d85b-cc08-4dad-8267-a4bb94c7229c.png

我們可以這樣簡單去理解這個(gè)方案:我們把共享的Flutter View當(dāng)成一個(gè)畫布戈轿,然后用一個(gè)Native的容器作為邏輯的頁面凌受。每次在打開一個(gè)容器的時(shí)候我們通過通信機(jī)制通知Flutter View繪制成當(dāng)前的邏輯頁面,然后將Flutter View放到當(dāng)前容器里面思杯。

頁面棧完全由原生控制胜蛉,每一個(gè)flutter頁面對應(yīng)一個(gè)原生容器(ViewControllerActivity),原生端創(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) {
                                                        }];

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末巷帝,一起剝皮案震驚了整個(gè)濱河市忌卤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌楞泼,老刑警劉巖驰徊,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笤闯,死亡現(xiàn)場離奇詭異,居然都是意外死亡棍厂,警方通過查閱死者的電腦和手機(jī)颗味,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牺弹,“玉大人浦马,你說我怎么就攤上這事≌牌” “怎么了捐韩?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鹃锈。 經(jīng)常有香客問我荤胁,道長,這世上最難降的妖魔是什么屎债? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任仅政,我火速辦了婚禮,結(jié)果婚禮上盆驹,老公的妹妹穿的比我還像新娘圆丹。我一直安慰自己,他們只是感情好躯喇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布辫封。 她就那樣靜靜地躺著,像睡著了一般廉丽。 火紅的嫁衣襯著肌膚如雪倦微。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天正压,我揣著相機(jī)與錄音欣福,去河邊找鬼。 笑死焦履,一個(gè)胖子當(dāng)著我的面吹牛拓劝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘉裤,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼郑临,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了屑宠?” 一聲冷哼從身側(cè)響起厢洞,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后犀变,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秋柄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年获枝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骇笔。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡省店,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笨触,到底是詐尸還是另有隱情懦傍,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布芦劣,位于F島的核電站粗俱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏虚吟。R本人自食惡果不足惜寸认,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望串慰。 院中可真熱鬧偏塞,春花似錦、人聲如沸邦鲫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庆捺。三九已至古今,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間滔以,已是汗流浹背沧卢。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留醉者,地道東北人但狭。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像撬即,于是被迫代替她去往敵國和親立磁。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容