iOS端Flutter混合工程及交互實踐

[TOC]ios

混合工程搭建

爲了項目能夠支持Flutter和Native混合開發的模式,咱們須要在對原生項目無侵入的條件下接入flutter,原生項目直接依賴flutter項目產物,以下圖所示:git

Flutter官方文檔提供的混合方案

1.建立Flutter工程

安裝flutter,自行百度;任意目錄下執行flutter create -t module my_flutter"my_flutter"是要建立的 Flutter 工程的名稱。shell

2.經過 Cocoapods 將 Flutter 引入 現有 Native 工程

Podfile添加如下下代碼json

flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
複製代碼

而後執行pod install 這個ruby腳本主要作下面4件事情:xcode

  • 解析 '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腳本的做用:ruby

  • build: 根據當前 Xcode 工程的 'configuration' 和其餘編譯配置編譯 Flutter 工程
  • embed: 將 build 出來的 framework、資源包放入 Xcode 編譯目錄,並簽名 framework

這裏就有了一個問題,Flutter 工程依賴 Native工程來執行編譯,影響Native工程的開發流程與打包流程,開發Native的人也須要安裝Flutter環境才能調試APPbash

4.總結

以上操做能夠簡單的理解爲,Native工程配置好腳本後,運行時會先編譯Flutter項目,Flutter項目會在本身的相應目錄生成Flutter.framework、依賴的Native插件等產物,最終在pod中配置好路徑等參數,經過pod本地依賴的方式集成了flutter。網絡

實現無侵入Native Flutter 混合工程

基於官方的方案,爲了實現這個目標,須要實現如下2點:app

  1. Flutter 工程裏建立一個打包腳本,能夠產生 Flutter 工程產物並上傳到遠程倉庫;
  2. 在 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 產物目錄
  • 經過 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頁面對應一個原生容器(ViewControllerActivity),原生端建立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':{},
                    routeCallback: (Map<dynamic, 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,
        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.頁面導航

Flutter頁面棧由原生控制,使用本身的導航欄。關閉不一樣頁面的方法

// 關閉返回上一頁
static Future<bool> closeCurPage()
// 返回到特定頁面,使用openUrl交互
ACRouter.openUrl('mizlicai://product/closeToRoot', param,
        routeCallback: (Map<dynamic, 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__, 'flutterhelper.rb')), binding)

複製代碼

AppDelegate中,初始化flutterboost,傳入FlutterRouter

#import "FlutterRouter.h"
- (void)startFlutter {

    [FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:[FlutterRouter sharedRouter]

                                                        onStart:^(FlutterViewController *fvc) {
                                                        }];

}
複製代碼
相關文章
相關標籤/搜索