今天,我來和你聊聊如何在原生應用中接入 Flutter。
在前面兩篇文章中,我與你分享了如何在 Dart 層引入 Android/iOS 平臺特定的能力,來提升 App 的功能體驗。
使用 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 文件:
~~~
use_frameworks!
platform :ios, '8.0'
target 'iOSDemo' do
#todo
end
~~~
然后,在命令行輸入 pod install 后,會自動生成一個 iOSDemo.xcworkspace 文件,這時我們就完成了 iOS 工程改造。
## Flutter 混編方案介紹
如果你想要在已有的原生 App 里嵌入一些 Flutter 頁面,有兩個辦法:
* 將原生工程作為 Flutter 工程的子工程,由 Flutter 統一管理。這種模式,就是統一管理模式。
* 將 Flutter 工程作為原生工程共用的子模塊,維持原有的原生工程管理方式不變。這種模式,就是三端分離模式。
:-: 
圖 1 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 打開它,其目錄如下圖所示:
:-: 
圖 2 Flutter 模塊工程結構
可以看到,和傳統的 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
}
~~~
最后點擊運行,可以看到一個寫著“Hello from Flutter”的全屏紅色的 Flutter Widget 就展示出來了。至此,我們完成了 Android 工程的接入。
:-: 
圖 3 Android 工程接入示例
### iOS 模塊集成
iOS 工程接入的情況要稍微復雜一些。在 iOS 平臺,原生工程對 Flutter 的依賴分別是:
* Flutter 庫和引擎,即 Flutter.framework;
* Flutter 工程的產物,即 App.framework。
iOS 平臺的 Flutter 模塊抽取,實際上就是通過打包命令生成這兩個產物,并將它們封裝成一個 pod 供原生工程引用。
類似地,首先我們在 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 也展示出來了。至此,iOS 工程的接入我們也順利搞定了。
:-: 
圖 4 iOS 工程接入示例
## 總結
通過分離 Android、iOS 和 Flutter 三端工程,抽離 Flutter 庫和引擎及工程代碼為組件庫,以 Android 和 iOS 平臺最常見的 aar 和 pod 形式接入原生工程,我們就可以低成本地接入 Flutter 模塊,愉快地使用 Flutter 擴展原生 App 的邊界了。
但,我們還可以做得更好。
如果每次通過構建 Flutter 模塊工程,都是手動搬運 Flutter 編譯產物,那很容易就會因為工程管理混亂導致 Flutter 組件庫被覆蓋,從而引發難以排查的 Bug。而要解決此類問題的話,我們可以引入 CI 自動構建框架,把 Flutter 編譯產物構建自動化,原生工程通過接入不同版本的構建產物,實現更優雅的三端分離模式。
而關于自動化構建,我會在后面的文章中和你詳細介紹,這里就不再贅述了。
接下來,我們簡單回顧一下今天的內容。
原生工程混編 Flutter 的方式有兩種。一種是,將 Flutter 工程內嵌 Android 和 iOS 工程,由 Flutter 統一管理的集中模式;另一種是,將 Flutter 工程作為原生工程共用的子模塊,由原生工程各自管理的三端工程分離模式。目前,業界采用的基本都是第二種方式。
而對于三端工程分離模式最主要的則是抽離 Flutter 工程,將不同平臺的構建產物依照標準組件化的形式進行管理,即:針對 Android 平臺打包構建生成 aar,通過 build.gradle 進行依賴管理;針對 iOS 平臺打包構建生成 framework,將其封裝成獨立的 pod,并通過 podfile 進行依賴管理。
我把今天分享所涉及到的知識點打包到了 GitHub([flutter\_module\_page](https://github.com/cyndibaby905/28_module_page)、[iOS\_demo](https://github.com/cyndibaby905/28_iOSDemo)、[Android\_Demo](https://github.com/cyndibaby905/28_AndroidDemo))中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你下留一個思考題吧。
對于有資源依賴的 Flutter 模塊工程而言,其打包構建的產物,以及抽離 Flutter 組件庫的過程會有什么不同嗎?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略