1. 程式人生 > IOS開發 >iOS端Flutter混合工程及互動實踐

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

  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':{},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) {
                                                        }];

}
複製程式碼