Flutter原生混合開發

xiangzhihong發表於2019-11-08

使用 Flutter 從頭開始寫一個 App是一件輕鬆愜意的事情。但是對於成熟產品來說,完全摒棄原有 App 的歷史沉澱,全面轉向 Flutter 並不現實。用 Flutter 去統一 iOS/Android 技術棧,把它作為已有原生 App 的擴充套件,然後通過逐步試驗有序推進從而提升終端開發效率,可能才是現階段 Flutter 最有效的整合方式。

那麼,Flutter 工程與原生工程該如何組織管理?不同平臺的 Flutter 工程打包構建產物該如何抽取封裝?封裝後的產物該如何引入原生工程?原生工程又該如何使用封裝後的 Flutter 能力?

這些問題使得在已有原生 App 中接入 Flutter 看似並不是一件容易的事情。那接下來,我就和你介紹下如何在原生 App 中以最自然的方式接入 Flutter。

準備工作

既然要在原生應用中混編 Flutter,相信你一定已經準備好了原生應用工程。如果你還沒有準備好也沒關係,我會以一個最小化的示例和你演示這個改造過程。

首先,我們分別用 Xcode 與 Android Studio 快速建立一個只有首頁的基本工程,工程名分別為 iOSDemo 與 AndroidDemo。

到此,Android 工程就已經準備好了;而對於 iOS 工程來說,由於基本工程並不支援以元件化的方式管理專案,因此我們還需要多做一步,將其改造成使用 CocoaPods 管理的工程,也就是要在 iOSDemo 根目錄下建立一個只有基本資訊的 Podfile 檔案。Podfile檔案的配置如下:


use_frameworks!
platform :ios, '8.0'
target 'iOSDemo' do
#todo
end
複製程式碼

然後,在命令列輸入 pod install 命令後,會自動生成一個 iOSDemo.xcworkspace 檔案,該檔案存放的就是我們專案需要的依賴庫,這時我們就完成了 iOS 工程改造。

混編方案

如果你想要在已有的原生 App 裡嵌入一些 Flutter 頁面,有兩個辦法可以實現,即統一管理模式和三端分離模式。

  • 將原生工程作為 Flutter 工程的子工程,由 Flutter 統一管理。這種模式,就是統一管理模式。
  • 將 Flutter 工程作為原生工程共用的子模組,維持原有的原生工程管理方式不變。這種模式,就是三端分離模式。

在這裡插入圖片描述

由於 Flutter 早期提供的混編方式能力及相關資料有限,國內較早使用 Flutter 混合開發的團隊大多使用的是統一管理模式。但是,隨著功能迭代的深入,這種方案的弊端也隨之顯露,不僅三端(Android、iOS、Flutter)程式碼耦合嚴重,相關工具鏈耗時也隨之大幅增長,導致開發效率降低。

所以,後續使用 Flutter 混合開發的團隊陸續按照三端程式碼分離的模式來進行依賴治理,實現了 Flutter 工程的輕量級接入。

除此之外,三端程式碼分離模式還可以把 Flutter 模組作為原生工程的子模組,從而快速實現 Flutter 功能的“熱插拔”,降低原生工程改造的成本。而 Flutter 工程通過 Android Studio 進行管理,無需開啟原生工程,可直接進行 Dart 程式碼和原生程式碼的開發除錯。

三端工程分離模式的關鍵是抽離 Flutter 工程,將不同平臺的構建產物依照標準元件化的形式進行管理,即 Android 使用 aar、iOS 使用 pod。換句話說,接下來介紹的混編方案會將 Flutter 模組打包成 aar 和 pod,這樣原生工程就可以像引用其他第三方原生元件庫那樣快速接入 Flutter 了。

整合Flutter準備

當我們建立一個新的Flutter 工程時,除了一些通用配置外,Flutter還包括 Flutter 工程和原生工程的目錄(即 iOS 和 Android 兩個目錄)。在這種情況下,原生工程就會依賴於 Flutter 相關的庫和資源,從而無法脫離父目錄進行獨立構建和執行。

原生工程對 Flutter 的依賴主要分為兩部分:

  • Flutter 庫和引擎,也就是 Flutter 的 Framework 庫和引擎庫;
  • Flutter 工程,也就是我們自己實現的 Flutter 模組功能,主要包括 Flutter 工程 lib 目錄下的 Dart 程式碼實現的這部分功能。

在已經有原生工程的情況下,我們需要在同級目錄建立 Flutter 模組,構建 iOS 和 Android 各自的 Flutter 依賴庫。這也很好實現,Flutter 就為我們提供了這樣的命令。我們只需要在原生專案的同級目錄下,執行 Flutter 命令建立名為 flutter_library 的模組即可,命令如下。


Flutter create -t module flutter_library
複製程式碼

這裡的 Flutter 模組,也是 Flutter 工程,我們用 Android Studio 開啟它,其目錄如下圖所示。

在這裡插入圖片描述
可以看到,和傳統的 Flutter 工程相比,Flutter 模組工程也有內嵌的 Android 工程與 iOS 工程,因此我們可以像普通工程一樣使用 Android Studio 進行開發除錯。

仔細檢視可以發現,Flutter 模組有一個細微的變化:Android 工程下多了一個 Flutter 目錄,這個目錄下的 build.gradle 配置就是我們構建 aar 的打包配置。這就是模組工程既能像 Flutter 傳統工程一樣使用 Android Studio 開發除錯,又能打包構建 aar 與 pod 的祕密。

實際上,iOS 工程的目錄結構也有細微變化,但這個差異並不影響打包構建,因此此處就不再展開了。

然後,我們開啟 main.dart 檔案,將其邏輯更新為以下程式碼邏輯,即一個寫著“Hello from Flutter”的全屏紅色的 Flutter Widget,如下所示。


import 'package:flutter/material.dart';
import 'dart:ui';

void main() => runApp(_widgetForRoute(window.defaultRouteName));//獨立執行傳入預設路由

Widget _widgetForRoute(String route) {
  switch (route) {
    default:
      return MaterialApp(
        home: Scaffold(
          backgroundColor: const Color(0xFFD63031),//ARGB紅色
          body: Center(
            child: Text(
              'Hello from Flutter', //顯示的文字
              textDirection: TextDirection.ltr,
              style: TextStyle(
                fontSize: 20.0,
                color: Colors.blue,
              ),
            ),
          ),
        ),
      );
  }
}
複製程式碼

我們建立的 Widget 實際上是包在一個 switch-case 語句中的。這是因為封裝的 Flutter 模組一般會有多個頁面級 Widget,原生 App 程式碼則會通過傳入路由標識字串,告訴 Flutter 究竟應該返回何種 Widget。為了簡化案例,在這裡我們忽略標識字串,統一返回一個 MaterialApp。

接下來,我們要做的事情就是把這段程式碼編譯打包,構建出對應的 Android 和 iOS 依賴庫,實現原生工程的接入。

現在,我們首先來看看 Android 工程如何接入。

Android原生整合

之前我們提到原生工程對 Flutter 的依賴主要分為兩部分,對應到 Android 平臺,這兩部分分別是:

  • Flutter 庫和引擎,也就是 icudtl.dat、libFlutter.so,還有一些 class 檔案。這些檔案都封裝在 Flutter.jar 中。
  • Flutter 工程產物,主要包括應用程式資料段 isolate_snapshot_data、應用程式指令段 isolate_snapshot_instr、虛擬機器資料段 vm_snapshot_data、虛擬機器指令段 vm_snapshot_instr和資原始檔 Flutter_assets等內容。

搞清楚 Flutter 工程的 Android 編譯產物之後,我們需要對 Android 的 Flutter 依賴進行抽取,步驟如下。

首先,在 Flutter_library 的根目錄下,執行 aar 打包構建命令,如下所示。

Flutter build apk --debug
複製程式碼

這條命令的作用是編譯工程產物,並將 Flutter.jar 和工程產物編譯結果封裝成一個 aar,如下圖所示。

在這裡插入圖片描述
你很快就會想到,如果是構建 release 產物,只需要把 debug 換成 release 就可以了。

打包構建的 flutter-debug.aar 位於.android/Flutter/build/outputs/aar/ 目錄下,我們把它拷貝到原生 Android 工程 AndroidDemo 的 app/libs 目錄下,並在 App 的打包配置 build.gradle 中新增對它的依賴。


...
repositories {
    flatDir {
        dirs 'libs'   // aar目錄
    }
}
android {
    ...
    compileOptions {
        sourceCompatibility 1.8 //Java 1.8
        targetCompatibility 1.8 //Java 1.8
    }
    ...
}

dependencies {
    ...
    implementation(name: 'flutter-debug', ext: 'aar')//Flutter模組aar
    ...
}
複製程式碼

Sync 一下專案,Flutter 模組就被新增到了 Android 專案中。

然後,我們試著改一下 MainActivity.java 的程式碼,把它的 contentView 改成 Flutter 的 widget,如下所示。


protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); //傳入路由識別符號
    setContentView(FlutterView);//用FlutterView替代Activity的ContentView
}
複製程式碼

重新執行Android原生工程,效果如下圖。

在這裡插入圖片描述

iOS 原生整合

iOS 工程接入的情況要稍微複雜一些。在 iOS 平臺,原生工程對 Flutter 的依賴分別是:

  • Flutter 庫和引擎,即 Flutter.framework;
  • Flutter 工程的產物,即 App.framework。

iOS 平臺的 Flutter 模組抽取,實際上就是通過打包命令生成這兩個產物,並將它們封裝成一個 pod 供iOS原生工程引用。

類似地,首先我們在 Flutter_library 的根目錄下,執行 iOS 打包構建命令。


Flutter build ios --debug
複製程式碼

這條命令的作用是編譯 Flutter 工程生成兩個產物:Flutter.framework 和 App.framework。同樣,把 debug 換成 release 就可以構建 release 產物(當然,你還需要處理一下簽名問題)。

然後,在 iOSDemo 的根目錄下建立一個名為 FlutterEngine 的目錄,並把這兩個 framework 檔案拷貝進去。iOS 的模組化產物工作要比 Android 多一個步驟,因為我們需要把這兩個產物手動封裝成 pod。因此,我們還需要在該目錄下建立 FlutterEngine.podspec,即 Flutter 模組的元件定義。


Pod::Spec.new do |s|
  s.name             = 'FlutterEngine'
  s.version          = '0.1.0'
  s.summary          = 'XXXXXXX'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC
  s.homepage         = 'https://github.com/xx/FlutterEngine'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'chenhang' => 'hangisnice@gmail.com' }
  s.source       = { :git => "", :tag => "#{s.version}" }
  s.ios.deployment_target = '8.0'
  s.ios.vendored_frameworks = 'App.framework', 'Flutter.framework'
end
複製程式碼

然後,執行pod lib lint 命令,Flutter 模組元件就已經做好了。接下來,我們再修改 Podfile 檔案把它整合到 iOSDemo 工程中,新增如下指令碼。


...
target 'iOSDemo' do
    pod 'FlutterEngine', :path => './'
end
複製程式碼

然後,執行pod install 命令,Flutter 模組就整合進 iOS 原生工程中了。再次,我們試著修改一下 AppDelegate.m 的程式碼,把 window 的 rootViewController 改成 FlutterViewController,如下所示。


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    FlutterViewController *vc = [[FlutterViewController alloc]init];
    [vc setInitialRoute:@"defaultRoute"]; //路由識別符號
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}
複製程式碼

最後執行原生專案,一個寫著“Hello from Flutter”的全屏紅色的 Flutter Widget 也展示出來了,如下圖所示。

在這裡插入圖片描述

總結

在原生工程中整合Flutter是現階段最常見的方式。通過分離 Android、iOS 和 Flutter 三端工程,抽離 Flutter 庫和引擎及工程程式碼為元件庫,以 Android 和 iOS 平臺最常見的 aar 和 pod 形式接入原生工程,從而將不同平臺的構建產物依照標準元件化的形式進行管理。

如果每次通過構建 Flutter 模組工程,都是手動搬運 Flutter 編譯產物,那很容易就會因為工程管理混亂導致 Flutter 元件庫被覆蓋,從而引發難以排查的 Bug。而要解決此類問題的話,我們可以引入 CI 自動構建框架,把 Flutter 編譯產物構建自動化,原生工程通過接入不同版本的構建產物,實現更優雅的三端分離模式。

相關文章