構建屬於自己的Flutter混合開發框架

xiangzhihong發表於2020-02-19

所謂混合開發,指的是 App 的整體架構以原生技術棧為基礎,將 Flutter 執行環境嵌入到原生 App 工程中,然後由原生開發人員為 Flutter 執行提供宿主容器及基礎能力支撐,而 Flutter 開發人員則負責應用層業務及 App 內大部分渲染工作。

在這種開發模式下,好處十分明顯。對於工程師而言,跨平臺的 Flutter 框架減少了對底層環境的依賴,使用完整的技術棧和工具鏈隔離了各個終端系統的差異,無論是 Android、iOS 甚至是前端工程師,都可以使用統一而標準化的能力進行業務開發,從而擴充了技能棧。而對於企業而言,這種方式不僅具備了原生 App 良好的使用者體驗,以及豐富的底層能力,還同時擁有了跨平臺技術開發低成本和多端體驗一致性的優勢,直接節省研發資源。

那麼,在原生工程中引入 Flutter 混合開發能力,我們應該如何設計工程架構,原生開發與 Flutter 開發的工作模式又是怎樣的呢?

混合開發架構

與純 Flutter 工程能夠以自治的方式去分拆軟體功能、管理工程依賴不同,Flutter 混合工程的功能分治需要原生工程與 Flutter 工程一起配合完成,即:在 Flutter 模組的視角看來,一部分與渲染相關的基礎能力完全由 Flutter 程式碼實現,而另一部分涉及作業系統底層、業務通用能力部分,以及整體應用架構支撐,則需要藉助於原生工程給予支援。

我們可以通過四象限分析法,把純 Flutter 應用按照業務和 UI 分解成 4 類。同樣,混合工程的功能單元也可以按照這個分治邏輯分為 4 個維度,即不具備業務屬性的原生基礎功能、不具備業務屬性的原生 UI 控制元件、不具備 UI 屬性的原生基礎業務功能和帶 UI 屬性的獨立業務模組,如下圖所示。

在這裡插入圖片描述
從圖中可以看到,對於前 3 個維度(即原生 UI 控制元件、原生基礎功能、原生基礎業務功能)的定義,純 Flutter 工程與混合工程並無區別,只不過實現的方式由 Flutter 變成了原生;對於第四個維度(即獨立業務模組)的功能歸屬,考慮到業務模組的最小單元是頁面,而 Flutter 的最終呈現形式也是獨立的頁面,因此我們把 Flutter 模組也歸為此類,我們的工程可以像依賴原生業務模組一樣直接依賴它,為使用者提供獨立的業務功能。當我們把這些元件及其依賴按照從上到下的方式進行劃分,然後再整體看,就是一個完整的混合開發架構了,整個架構下圖所示。

在這裡插入圖片描述

可以看到,原生工程和 Flutter 工程的邊界定義清晰,雙方都可以保持原有的分層管理依賴的開發模式不變。需要注意的是,作為一個內嵌在原生工程的外掛,Flutter 模組的執行環境是由原生工程提供支援的,這也就意味著在渲染互動能力之外的部分基礎功能(比如網路、儲存),以及和原生業務共享的業務通用能力(比如支付、賬號)需要原生工程配合完成,即原生工程以分層的形式提供上層呼叫介面,Flutter 模組以外掛的形式直接訪問原生程式碼宿主對應功能實現。

因此,不僅不同歸屬定義的原生元件之前存在著分層依賴的關係,Flutter 模組與原生元件之前也隱含著分層依賴的關係。比如,Flutter 模組中處於基礎業務模組的賬號外掛,依賴位於原生基礎業務模組中的賬號功能;Flutter 模組中處於基礎業務模組的網路外掛,依賴位於原生基礎功能的網路引擎庫。

在混合工程架構中,像原生工程依賴 Flutter 模組、Flutter 模組又依賴原生工程這樣跨技術棧的依賴管理行為,實際上是通過將雙方抽象為彼此對應技術棧的依賴,從而實現分層管理的:即將原生對 Flutter 的依賴抽象為依賴 Flutter 模組所封裝的原生元件,而 Flutter 對原生的依賴則抽象為依賴外掛所封裝的原生行為。

Flutter 混合開發流程

在常規的軟體開發流程中,工程師的職責涉及從需求到上線的整個生命週期,包含需求階段 -> 方案階段 -> 開發階段 -> 釋出階段 -> 線上運維階段,這其實就是一種抽象的工作流程。

其中,和工程化關聯最為緊密的是開發階段和釋出階段。我們可以將工作流中和工程開發相關的部分抽離定義為開發工作流,根據生命週期中關鍵節點和高頻節點,可以將整個工作流劃分為如下七個階段,即初始化 -> 開發 / 除錯 -> 構建 -> 測試 -> 釋出 -> 整合 -> 原生工具鏈。下圖演示了Flutter和原生開發的工作流。

在這裡插入圖片描述
其中,前 6 個階段是 Flutter 的標準工作流,最後一個階段是原生開發的標準工作流。可以看到,在混合開發工作模式中,Flutter 的開發模式與原生開發模式之間有著清晰的分工邊界:Flutter 模組是原生工程的上游,其最終產物是原生工程的依賴物件。從原生工程視角看,其開發模式與普通原生應用並無區別。

對於 Flutter 標準工作流的 6 個階段而言,每個階段都會涉及業務或產品特性提出的特異性要求,技術方案的選型,各階段工作成本可用性、可靠性的衡量,以及監控相關基礎服務的接入和配置等。每件事兒都是一個固定的步驟,而當開發規模隨著文件、程式碼、需求增加時,我們會發現重複的步驟越來越多。此時,如果我們把這些步驟像抽象程式碼一樣,抽象出一些相同操作,就可以大大提升開發效率。

優秀的程式設計師會發掘工作中的問題,從中探索提高生產力的辦法,而轉變思維模式就是一個不錯的起點。以持續交付的指導思想來看待這些問題,我們希望整體方案能夠以可重複、可配置化的形式,來保障整個工作流的開發體驗、效率、穩定性和可靠性,而這些都離不開 Flutter 對命令列工具支援。

比如,對於測試階段的 Dart 程式碼分析,我們可以使用 flutter analyze 命令對程式碼中可能存在的語法或語義問題進行檢查;又比如,在釋出期的 package 釋出環節,我們可以使用 flutter packages pub publish --dry-run 命令對待發布的包進行釋出前檢查,確認無誤後使用去掉 dry-run 引數的 publish 命令將包提交至 Pub 站點。

這些基本命令對各個開發節點的輸入、輸出以及執行過程進行了抽象,熟練掌握它們及對應的擴充套件引數用法,我們不僅可以在本地開發時打造一個易用便捷的工程開發環境,還可以將這些命令部署到雲端,實現工程構建及部署的自動化。在Flutter 標準工作流中,常用的命令如下所示。

在這裡插入圖片描述

混合開發的基本設計原則

在混合開發中,我們需要重點關注的是專案的基本設計原則,即確定分工邊界。下面從工程架構維度和工作模式維度來進行拆分。

在工程架構維度,由於 Flutter 模組作為原生工程的一個業務依賴,其執行環境是由原生工程提供的,因此我們需要將它們各自抽象為對應技術棧的依賴管理方式,以分層依賴的方式確定二者的邊界。

而在工作模式維度,考慮到 Flutter 模組開發是原生開發的上游,因此我們只需要從其構建產物的過程入手,抽象出開發過程中的關鍵節點和高頻節點,以命令列的形式進行統一管理。構建產物是 Flutter 模組的輸出,同時也是原生工程的輸入,一旦產物完成構建,我們就可以接入原生開發的工作流了。

在 Flutter 混合框架中,Flutter 模組與原生工程是相互依存、互利共贏的關係。

  • Flutter 跨平臺開發效率高,渲染效能和多端體驗一致性好,因此在分工上主要專注於實現應用層的獨立業務(頁面)的渲染閉環;
  • 原生開發穩定性高,精細化控制力強,底層基礎能力豐富,因此在分工上主要專注於提供整體應用架構,為 Flutter 模組提供穩定的執行環境及對應的基礎能力支援。

那麼,在原生工程中為 Flutter 模組提供基礎能力支撐的過程中,面對跨技術棧的依賴管理,我們該遵循何種原則呢?對於 Flutter 模組及其依賴的原生外掛們,我們又該如何以標準的原生工程依賴形式進行元件封裝呢?下面重點看一下原生工程是如何進行外掛管理的。

可以看到,在原生 App 工程中引入 Flutter 執行環境,由原生開發主做應用架構和基礎能力賦能、Flutter 開發主做應用層業務的混合開發協作方式,能夠綜合原生 App 與 Flutter 框架雙方的特點和優勢,不僅可以直接節省研發資源,也符合目前行業人才能力模型的發展趨勢。

原生外掛管理

在Flutter 應用中,Dart 程式碼提供原生能力支援主要有兩種方式,即在原生工程中的 Flutter 應用入口註冊原生程式碼宿主回撥的輕量級方案,以及使用外掛工程進行獨立拆分封裝的工程化解耦方案。

不過,無論使用哪種方式,Flutter 應用工程提供的標準解決方案,都能夠在整合構建時自動管理原生程式碼宿主及其相應的原生依賴,然後只需要在應用層使用 pubspec.yaml 檔案去管理 Dart 的依賴即可。

但對於混合工程而言,依賴關係的管理則會複雜一些。這是因為與 Flutter 應用工程有著對原生元件簡單清晰的單向依賴關係不同,混合工程對原生元件的依賴關係是多向的,即Flutter 模組工程會依賴原生元件,而原生工程的元件之間也會互相依賴。

如果繼續使用Flutter 的工具鏈管理原生元件的依賴關係,那麼整個工程就會陷入不穩定的狀態之中。因此,對於混合工程的原生依賴,Flutter 模組並不需要介入,完全交由原生工程進行統一管理才是正確的做法。而 Flutter 模組工程對原生工程的依賴,體現在依賴原生程式碼宿主提供的底層基礎能力的原生外掛上。

下面我們就以網路通訊這一基礎能力為例,展開說明原生工程與 Flutter 模組工程之間應該如何管理依賴關係。

網路外掛依賴管理實踐

眾所周知,在 Flutter開發中,我們可以使用 HttpClient、http 與 dio 這三種通訊方式來實現與服務端的資料交換。不過,在混合工程中,考慮到原生元件也需要使用網路通訊能力,所以通常是由原生工程來提供網路通訊功能,然後封裝後提供給Flutter使用。這樣,不僅可以在工程架構層面實現更合理的功能分治,還可以統一整個 App 內資料交換的行為。比如,在網路引擎中為介面請求增加通用引數,或者是集中攔截錯誤等。

在原生網路通訊方面,目前市面上有很多優秀的第三方開源 SDK,比如 iOS 的 AFNetworking 和 Alamofire、Android 的 OkHttp 和 Retrofit 等。考慮到 AFNetworking 和 OkHttp 在各自平臺的社群活躍度相對最高,因此下面就以它倆為例演示混合工程的原生外掛管理方法。

網路外掛封裝

要想搞清楚如何管理原生外掛,我們需要先使用方法通道來建立 Dart 層與原生程式碼宿主之間的聯絡。

1,Dart程式碼封裝

對於外掛工程的 Dart 層程式碼而言,由於它僅僅是原生工程的程式碼宿主代理,所以這一層的介面設計比較簡單,只需要提供一個可以接收請求 URL 和引數,並返回介面響應資料的方法即可 ,如下所示。


class FlutterPluginNetwork {
  ...
  static Future<String> doRequest(url,params)  async {
    //使用方法通道呼叫原生介面doRequest,傳入URL和param兩個引數
    final String result = await _channel.invokeMethod('doRequest', {
      "url": url,
      "param": params,
    });
    return result;
  }
}
複製程式碼

關於Flutter如何與原生進行互動,可以檢視我之前的文章:混合開發簡介

完成Dart 層介面封裝後,接下來再看一下 Android 和 iOS 程式碼宿主是如何響應 Dart 層的介面呼叫的。

2,原生端封裝

前面說過,原生程式碼的基礎通訊能力是基於 AFNetworking(iOS)和 OkHttp(Android)做的封裝,所以為了在原生程式碼中使用它們,我們首先需要分別在 flutter_plugin_network.podspec 和 build.gradle 檔案中新增外掛的依賴。對於iOS工程來說,在 flutter_plugin_network.podspec 檔案中,宣告工程對 AFNetworking 的依賴。


Pod::Spec.new do |s|
  ...
  s.dependency 'AFNetworking'
end
複製程式碼

對於Android原生工程來說, 在 build.gradle 檔案中新增對 OkHttp 的依賴,如下所示。


dependencies {
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
}
複製程式碼

然後,我們需要在原生介面 FlutterPluginNetworkPlugin 類中,完成例行的初始化外掛例項、繫結方法通道工作。最後,我們還需要在方法通道中取出對應的 URL 和 請求 引數,為 doRequest 方法分別提供 AFNetworking 和 OkHttp 的實現版本。

對於 iOS 的呼叫而言,由於 AFNetworking 的網路呼叫物件是 AFHTTPSessionManager 類,所以我們需要對這個類進行例項化,並定義其介面返回的序列化方式(本例中為字串),然後剩下的工作就是用它去發起網路請求,使用方法通道通知 Dart 層執行結果。


@implementation FlutterPluginNetworkPlugin
...
//方法通道回撥
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    //響應doRequest方法呼叫
    if ([@"doRequest" isEqualToString:call.method]) {
        //取出query引數和URL
        NSDictionary *arguments = call.arguments[@"param"];
        NSString *url = call.arguments[@"url"];
        [self doRequest:url withParams:arguments andResult:result];
    } else {
        //其他方法未實現
        result(FlutterMethodNotImplemented);
    }
}
//處理網路呼叫
- (void)doRequest:(NSString *)url withParams:(NSDictionary *)params andResult:(FlutterResult)result {
    //初始化網路呼叫例項
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //定義資料序列化方式為字串
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    NSMutableDictionary *newParams = [params mutableCopy];
    //增加自定義引數
    newParams[@"ppp"] = @"yyyy";
    //發起網路呼叫
    [manager GET:url parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        //取出響應資料,響應Dart呼叫
        NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
        result(string);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        //通知Dart呼叫失敗
        result([FlutterError errorWithCode:@"Error" message:error.localizedDescription details:nil]);
    }];
}
@end
複製程式碼

Android 的呼叫類似,OkHttp的網路呼叫物件是 OkHttpClient 類,所以我們同樣需要對這個類進行例項化。OkHttp的預設序列化方式就是字串,所以我們什麼都不用做,只需要 URL 引數加工成 OkHttp 期望的格式,然後就是用它去發起網路請求,使用方法通道通知 Dart 層執行結果即可。


public class FlutterPluginNetworkPlugin implements MethodCallHandler {
  ...
  @Override
  //方法通道回撥
  public void onMethodCall(MethodCall call, Result result) {
    //響應doRequest方法呼叫
    if (call.method.equals("doRequest")) {
      //取出query引數和URL
      HashMap param = call.argument("param");
      String url = call.argument("url");
      doRequest(url,param,result);
    } else {
      //其他方法未實現
      result.notImplemented();
    }
  }
  //處理網路呼叫
  void doRequest(String url, HashMap<String, String> param, final Result result) {
    //初始化網路呼叫例項
    OkHttpClient client = new OkHttpClient();
    //加工URL及query引數
    HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
    for (String key : param.keySet()) {
      String value = param.get(key);
      urlBuilder.addQueryParameter(key,value);
    }
    //加入自定義通用引數
    urlBuilder.addQueryParameter("ppp", "yyyy");
    String requestUrl = urlBuilder.build().toString();

    //發起網路呼叫
    final Request request = new Request.Builder().url(requestUrl).build();
    client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Call call, final IOException e) {
        //切換至主執行緒,通知Dart呼叫失敗
        registrar.activity().runOnUiThread(new Runnable() {
          @Override
          public void run() {
            result.error("Error", e.toString(), null);
          }
        });
      }
      
      @Override
      public void onResponse(Call call, final Response response) throws IOException {
        //取出響應資料
        final String content = response.body().string();
        //切換至主執行緒,響應Dart呼叫
        registrar.activity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
              result.success(content);
            }
        });
      }
    });
  }
}
複製程式碼

需要注意的是,由於方法通道是非執行緒安全的,所以原生程式碼與 Flutter 之間所有的介面呼叫必須發生在主執行緒。而 OktHtp 在處理網路請求時,由於涉及非主執行緒切換,所以需要呼叫 runOnUiThread 方法以確保回撥過程是在 UI 執行緒中執行的,否則應用可能會出現奇怪的 Bug,甚至是 Crash。

有些同學可能會有疑問,為什麼 doRequest 的 Android 實現需要手動切回 UI 執行緒,而 iOS 實現則不需要呢?這其實是因為 doRequest 的 iOS 實現背後依賴的 AFNetworking,已經在資料回撥介面時為我們主動切換了 UI 執行緒,所以我們自然不需要重複再做一次了。

在完成了原生介面封裝之後,Flutter 工程所需的網路通訊功能的介面實現,就全部搞定了。

Flutter 模組工程依賴管理

通過上面這些步驟,我們以外掛的形式提供了原生網路功能的封裝。接下來,我們就需要在 Flutter 模組工程中使用這個外掛,並提供對應的構建產物封裝,提供給原生工程使用了。

  • 第一,如何使用 FlutterPluginNetworkPlugin 外掛,也就是模組工程功能如何實現;
  • 第二,模組工程的 iOS 構建產物應該如何封裝,也就是原生 iOS 工程如何管理 Flutter 模組工程的依賴;
  • 第三,模組工程的 Android 構建產物應該如何封裝,也就是原生 Android 工程如何管理 Flutter 模組工程的依賴。

1,模組工程功能實現

為了使用 FlutterPluginNetworkPlugin 外掛實現與服務端的資料交換能力,我們首先需要在 pubspec.yaml 檔案中,將工程對它的依賴顯示地宣告出來,如下所示。


flutter_plugin_network:
    git:
      url: https://github.com/cyndibaby905/flutter_plugin_network.git
複製程式碼

然後,我們還得在 main.dart 檔案中為它提供一個觸發入口。在下面的示例程式碼中,我們在介面上顯示一個 RaisedButton 按鈕,在其點選回撥函式時使用 FlutterPluginNetwork 外掛發起了一次網路介面呼叫,並把網路返回的資料列印到了控制檯上,程式碼如下。


RaisedButton(
  child: Text("doRequest"),
  onPressed:()=>FlutterPluginNetwork.doRequest("https://jsonplaceholder.typicode.com/posts", {'userId':'2'}).then((s)=>print('Result:$s')),
)
複製程式碼

執行這段程式碼,點選 doRequest 按鈕時會觀察控制檯輸出,證明 Flutter 模組的功能表現是完全符合預期的。

在這裡插入圖片描述

構建產物封裝

我們都知道,模組工程的 Android 構建產物是 aar,iOS 構建產物是 Framework。Flutter外掛依賴的模組工程構建產物的兩種封裝方案,即手動封裝方案與自動化封裝方案。這兩種封裝方案,最終都會輸出同樣的組織形式(Android 是 aar,iOS 則是帶 podspec 的 Framework 封裝元件)。

如果我們的模組工程存在外掛依賴,又該如何進行封裝,它的封裝過程是否有區別呢?簡單的說,對於模組工程本身而言,這個過程沒有區別;但對於模組工程的外掛依賴來說,我們需要主動告訴原生工程,哪些依賴是需要它去管理的。

由於 Flutter 模組工程把所有原生的依賴都交給了原生工程去管理,因此其構建產物並不會攜帶任何原生外掛的封裝實現,所以我們需要遍歷模組工程所使用的原生依賴元件們,為它們逐一生成外掛程式碼對應的原生元件封裝。

在純Flutter 工程中,管理第三方依賴庫使用的是.packages 檔案儲存,它使用的是依賴包名與系統快取中的包檔案路徑。類似的,外掛依賴也可以使用類似的檔案進行統一管理,即.flutter-plugins。我們可以通過這個檔案,找到對應的外掛名字(本例中即為 flutter_plugin_network)及快取路徑,如下所示。


flutter_plugin_network=/Users/hangchen/Documents/flutter/.pub-cache/git/flutter_plugin_network-9b4472aa46cf20c318b088573a30bc32c6961777/
複製程式碼

同時,外掛快取本身也可以被視為一個 Flutter 模組工程,所以我們可以採用與模組工程類似的辦法,為它生成對應的原生元件封裝。

iOS 構建產物封裝

對於 iOS 而言,這個過程相對簡單些,所以我們先來看看模組工程的 iOS 構建產物封裝過程。

首先,在外掛工程的 iOS 目錄下,模組工程提供了帶 podspec 檔案的原始碼元件,podspec 檔案提供了元件的宣告(及其依賴),因此我們可以把這個目錄下的檔案拷貝出來,連同 Flutter 模組元件一起放到原生工程中的專用目錄,並寫到 Podfile 檔案中。


#Podfile
target 'iOSDemo' do
  pod 'Flutter', :path => 'Flutter'
  pod 'flutter_plugin_network', :path => 'flutter_plugin_network'
end
複製程式碼

原生工程會識別出元件本身及其依賴,並按照宣告的依賴關係依次遍歷,自動安裝。然後,我們就可以像使用不帶外掛依賴的模組工程一樣,把它引入到原生工程中,為其設定入口,並在 FlutterViewController 中展示 Flutter 模組的頁面了。

不過需要注意的是,由於 FlutterViewController 並不感知這個過程,因此不會主動初始化專案中的外掛,所以我們還需要在入口處手動將工程裡所有的外掛依次宣告出來,如下所示。


//AppDelegate.m:
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    //初始化Flutter入口
    FlutterViewController *vc = [[FlutterViewController alloc]init];
    //初始化外掛
    [FlutterPluginNetworkPlugin registerWithRegistrar:[vc registrarForPlugin:@"FlutterPluginNetworkPlugin"]];
    //設定路由識別符號
    [vc setInitialRoute:@"defaultRoute"]; 
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}
複製程式碼

然後,使用Xcode 執行這段程式碼,點選 doRequest 按鈕,如果可以看到介面返回的資料資訊能夠被正常列印,證明我們已經可以在原生 iOS 工程中順利的使用 Flutter 模組了。

在這裡插入圖片描述

Android 構建產物封裝

與 iOS 的外掛工程元件在 ios 目錄類似,Android 的外掛工程元件在 android 目錄下。對於 iOS 的外掛工程,我們可以直接將原始碼元件提供給原生工程,但對於 Andriod 的外掛工程來說,我們只能將 aar 元件提供給原生工程,所以我們不僅需要像 iOS 操作步驟那樣進入外掛的元件目錄,還需要藉助構建命令,為外掛工程生成 aar。使用下面的命令即可生成外掛工程的aar包。


cd android
./gradlew flutter_plugin_network:assRel
複製程式碼

命令執行完成之後,aar 就生成好了,aar 包位於 android/build/outputs/aar 目錄下,我們開啟外掛快取對應的路徑,提取出對應的 aar即可。我們把生成的外掛 aar,連同 Flutter 模組 的aar 一起放到原生工程的 libs 目錄下,最後在 build.gradle 檔案裡引入外掛工程,如下所示。


//build.gradle
dependencies {
    ...
    implementation(name: 'flutter-debug', ext: 'aar')
    implementation(name: 'flutter_plugin_network-debug', ext: 'aar')
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
    ...
}
複製程式碼

然後,我們就可以在原生工程中為其設定入口,在 FlutterView 中展示 Flutter 頁面,接下來就可以使用 Flutter 模組帶來的高效開發和高效能渲染能力了。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); 
        setContentView(FlutterView);
    }
}
複製程式碼

需要注意的是,與 iOS 外掛工程的 podspec 能夠攜帶元件依賴不同,Android 外掛工程的封裝產物 aar 本身不攜帶任何配置資訊。所以,如果外掛工程本身存在原生依賴(如 flutter_plugin_network 依賴 OkHttp ),我們是無法通過 aar 去告訴原生工程其所需的原生依賴的。對於這種情況,我們只需要在原生工程中的 build.gradle 檔案裡手動地將外掛工程的依賴的外掛(即 OkHttp)顯示地宣告出來即可,如下所示。

//build.gradle
dependencies {
    ...
    implementation(name: 'flutter-debug', ext: 'aar')
    implementation(name: 'flutter_plugin_network-debug', ext: 'aar')
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
    ...
}
複製程式碼

至此,混合模組工程及其外掛依賴封裝成原生元件的全部工作就完成了,接下來原生工程可以像使用一個普通的原生元件一樣去使用 Flutter 模組元件的功能了。在 Android Studio 中執行這段程式碼,並點選 doRequest 按鈕,可以看到,我們可以在原生 Android 工程中正常使用 Flutter 封裝的頁面元件了。

在這裡插入圖片描述
當然,考慮到手動封裝模組工程及其構建產物的過程,繁瑣且容易出錯,我們可以把這些步驟抽象成命令列指令碼,並把它部署到 Travis 上。這樣在 Travis 檢測到程式碼變更之後,就會自動將 Flutter 模組的構建產物封裝成原生工程期望的元件格式了。

總結

眾所周知,Flutter 模組工程的原生元件封裝形式是 aar(Android)和 Framework(Pod)。與純 Flutter 應用工程能夠自動管理外掛的原生依賴不同,混合工程的這部分工作在模組工程中是完全交給原生工程去管理的。因此,我們需要查詢記錄了外掛名稱及快取路徑對映關係的.flutter-plugins 檔案,提取出每個外掛所對應的原生元件封裝,整合到原生工程中。

相比iOS外掛管理來說,Android的外掛管理比較繁瑣。對於有著外掛依賴的 Android 元件封裝來說,由於 aar 本身並不攜帶任何配置資訊,因此其操作以手工為主:我們不僅要執行構建命令依次生成外掛對應的 aar,還需要將外掛自身的原生依賴拷貝至原生工程。

為了解決這一問題,業界出現了一種名為fat-aar的打包手段,它能夠將模組工程本身,及其相關的外掛依賴統一打包成一個大的 aar,從而省去了依賴遍歷和依賴宣告的過程,實現了更好的功能自治性。但這種解決方案存在一些較為明顯的不足,以下是使用中存在的一些問題:

  • 依賴衝突問題:如果原生工程與外掛工程都引用了同樣的原生依賴元件(OkHttp),則原生工程的元件引用其依賴時會產生合併衝突,因此在釋出時必須手動去掉原生工程的元件依賴。
  • 巢狀依賴問題:fat-aar 只會處理 embedded 關鍵字指向的這層一級依賴,而不會處理再下一層的依賴。因此,對於依賴關係複雜的外掛支援,我們仍需要手動處理依賴問題。
  • Gradle 版本限制問題:fat-aar 方案對 Gradle 外掛版本有限制,且實現方式並不是官方設計考慮的點,加之 Gradle API 變更較快,所以存在後續難以維護的問題。
  • 不更新。fat-aar 專案已經不再維護了,最近一次更新還是 2 年前,對Android的新版本存在較大的風險。

因此,fat-aar 並不是管理外掛工程依賴的好的解決方案,所以最好還是得老老實實地去遍歷外掛依賴,以持續交付的方式自動化生成 aar。

參考資料

1,Flutter 應用程式除錯
2,Flutter For Web入門實戰
3,Flutter開發之路由與導航
4,Flutter 必備開源專案
5,Flutter混合開發
6,Flutter的Hot Reload是如何做到的
7,《Flutter in action》開源
8,Flutter開發之JSON解析
9,Flutter開發之基礎Widgets
10,Flutter開發之導航與路由管理
11,Flutter開發之網路請求
12,Flutter基礎知識
13,Flutter開發之Dart語言基礎
14,Flutter入門與環境搭建
15,移動跨平臺方案對比:WEEX、React Native、Flutter和PWA
16,Flutter開發之非同步程式設計

相關文章