為了把 Flutter 引入到原生工程,我們需要把 Flutter 工程改造為原生工程的一個組件依賴,并以組件化的方式管理不同平臺的 Flutter 構建產物,即 Android 平臺使用 aar、iOS 平臺使用 pod 進行依賴管理。這樣,我們就可以在 Android 工程中通過 FlutterView,iOS 工程中通過 FlutterViewController,為 Flutter 搭建應用入口,實現 Flutter 與原生的混合開發方式。
我在[第 26 篇](https://time.geekbang.org/column/article/127601)文章中提到,FlutterView 與 FlutterViewController 是初始化 Flutter 的地方,也是應用的入口。可以看到,以混合開發方式接入 Flutter,與開發一個純 Flutter 應用在運行機制上并無任何區別,只需要原生工程為它提供一個畫板容器(Android 為 FlutterView,iOS 為 FlutterViewController),Flutter 就可以自己管理頁面導航棧,從而實現多個復雜頁面的渲染和切換。
關于純 Flutter 應用的頁面路由與導航,我已經在[第 21 篇文章](https://time.geekbang.org/column/article/118421)中與你介紹過了。今天這篇文章,我會為你講述在混合開發中,應該如何管理混合導航棧。
對于混合開發的應用而言,通常我們只會將應用的部分模塊修改成 Flutter 開發,其他模塊繼續保留原生開發,因此應用內除了 Flutter 的頁面之外,還會有原生 Android、iOS 的頁面。在這種情況下,Flutter 頁面有可能會需要跳轉到原生頁面,而原生頁面也可能會需要跳轉到 Flutter 頁面。這就涉及到了一個新的問題:如何統一管理原生頁面和 Flutter 頁面跳轉交互的混合導航棧。
接下來,我們就從這個問題入手,開始今天的學習吧。
## 混合導航棧
混合導航棧,指的是原生頁面和 Flutter 頁面相互摻雜,存在于用戶視角的頁面導航棧視圖中。
以下圖為例,Flutter 與原生 Android、iOS 各自實現了一套互不相同的頁面映射機制,即原生采用單容器單頁面(一個 ViewController/Activity 對應一個原生頁面)、Flutter 采用單容器多頁面(一個 ViewController/Activity 對應多個 Flutter 頁面)的機制。Flutter 在原生的導航棧之上又自建了一套 Flutter 導航棧,這使得 Flutter 頁面與原生頁面之間涉及頁面切換時,我們需要處理跨引擎的頁面切換。
:-: 
圖 1 混合導航棧示意圖
接下來,我們就分別看看從原生頁面跳轉至 Flutter 頁面,以及從 Flutter 頁面跳轉至原生頁面,應該如何處理吧。
### 從原生頁面跳轉至 Flutter 頁面
從原生頁面跳轉至 Flutter 頁面,實現起來比較簡單。
因為 Flutter 本身依托于原生提供的容器(iOS 為 FlutterViewController,Android 為 Activity 中的 FlutterView),所以我們通過初始化 Flutter 容器,為其設置初始路由頁面之后,就可以以原生的方式跳轉至 Flutter 頁面了。
如下代碼所示。對于 iOS,我們初始化一個 FlutterViewController 的實例,為其設置初始化頁面路由后,將其加入原生的視圖導航棧中完成跳轉。
對于 Android 而言,則需要多加一步。因為 Flutter 頁面的入口并不是原生視圖導航棧的最小單位 Activity,而是一個 View(即 FlutterView),所以我們還需要把這個 View 包裝到 Activity 的 contentView 中。在 Activity 內部設置頁面初始化路由之后,在外部就可以采用打開一個普通的原生視圖的方式,打開 Flutter 頁面了。
~~~
//iOS 跳轉至 Flutter 頁面
FlutterViewController *vc = [[FlutterViewController alloc] init];
[vc setInitialRoute:@"defaultPage"];// 設置 Flutter 初始化路由頁面
[self.navigationController pushViewController:vc animated:YES];// 完成頁面跳轉
//Android 跳轉至 Flutter 頁面
// 創建一個作為 Flutter 頁面容器的 Activity
public class FlutterHomeActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 設置 Flutter 初始化路由頁面
View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); // 傳入路由標識符
setContentView(FlutterView);// 用 FlutterView 替代 Activity 的 ContentView
}
}
// 用 FlutterPageActivity 完成頁面跳轉
Intent intent = new Intent(MainActivity.this, FlutterHomeActivity.class);
startActivity(intent);
~~~
### 從 Flutter 頁面跳轉至原生頁面
從 Flutter 頁面跳轉至原生頁面,則會相對麻煩些,我們需要考慮以下兩種場景:
* 從 Flutter 頁面打開新的原生頁面;
* 從 Flutter 頁面回退到舊的原生頁面。
首先,我們來看看 Flutter 如何打開原生頁面。
Flutter 并沒有提供對原生頁面操作的方法,所以不可以直接調用。我們需要通過方法通道(你可以再回顧下[第 26 篇](https://time.geekbang.org/column/article/127601)文章的相關內容),在 Flutter 和原生兩端各自初始化時,提供 Flutter 操作原生頁面的方法,并注冊方法通道,在原生端收到 Flutter 的方法調用時,打開新的原生頁面。
接下來,我們再看看如何從 Flutter 頁面回退到原生頁面。
因為 Flutter 容器本身屬于原生導航棧的一部分,所以當 Flutter 容器內的根頁面(即初始化路由頁面)需要返回時,我們需要關閉 Flutter 容器,從而實現 Flutter 根頁面的關閉。同樣,Flutter 并沒有提供操作 Flutter 容器的方法,因此我們依然需要通過方法通道,在原生代碼宿主為 Flutter 提供操作 Flutter 容器的方法,在頁面返回時,關閉 Flutter 頁面。
Flutter 跳轉至原生頁面的兩種場景,如下圖所示:
:-: 
圖 2 Flutter 頁面跳轉至原生頁面示意圖
**接下來,我們一起看看這兩個需要通過方法通道實現的方法,即打開原生頁面 openNativePage,與關閉 Flutter 頁面 closeFlutterPage,在 Android 和 iOS 平臺上分別如何實現。**
注冊方法通道最合適的地方,是 Flutter 應用的入口,即在 FlutterViewController(iOS 端)和 Activity 中的 FlutterView(Android 端)這兩個容器內部初始化 Flutter 頁面前。為了將 Flutter 相關的行為封裝到容器內部,我們需要分別繼承 FlutterViewController 和 Activity,在其 viewDidLoad 和 onCreate 初始化容器時,注冊 openNativePage 和 closeFlutterPage 這兩個方法。
iOS 端的實現代碼如下所示:
~~~
@interface FlutterHomeViewController : FlutterViewController
@end
@implementation FlutterHomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 聲明方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/navigation" binaryMessenger:self];
// 注冊方法回調
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// 如果方法名為打開新頁面
if([call.method isEqualToString:@"openNativePage"]) {
// 初始化原生頁面并打開
SomeOtherNativeViewController *vc = [[SomeOtherNativeViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
result(@0);
}
// 如果方法名為關閉 Flutter 頁面
else if([call.method isEqualToString:@"closeFlutterPage"]) {
// 關閉自身 (FlutterHomeViewController)
[self.navigationController popViewControllerAnimated:YES];
result(@0);
}
else {
result(FlutterMethodNotImplemented);// 其他方法未實現
}
}];
}
@end
~~~
Android 端的實現代碼如下所示:
~~~
// 繼承 AppCompatActivity 來作為 Flutter 的容器
public class FlutterHomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化 Flutter 容器
FlutterView flutterView = Flutter.createView(this, getLifecycle(), "defaultPage"); // 傳入路由標識符
// 注冊方法通道
new MethodChannel(flutterView, "samples.chenhang/navigation").setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// 如果方法名為打開新頁面
if(call.method.equals("openNativePage")) {
// 新建 Intent,打開原生頁面
Intent intent = new Intent(FlutterHomeActivity.this, SomeNativePageActivity.class);
startActivity(intent);
result.success(0);
}
// 如果方法名為關閉 Flutter 頁面
else if(call.method.equals("closeFlutterPage")) {
// 銷毀自身 (Flutter 容器)
finish();
result.success(0);
}
else {
// 方法未實現
result.notImplemented();
}
}
});
// 將 flutterView 替換成 Activity 的 contentView
setContentView(flutterView);
}
}
~~~
經過上面的方法注冊,我們就可以在 Flutter 層分別通過 openNativePage 和 closeFlutterPage 方法,來實現 Flutter 頁面與原生頁面之間的切換了。
在下面的例子中,Flutter 容器的根視圖 DefaultPage 包含有兩個按鈕:
* 點擊左上角的按鈕后,可以通過 closeFlutterPage 返回原生頁面;
* 點擊中間的按鈕后,會打開一個新的 Flutter 頁面 PageA。PageA 中也有一個按鈕,點擊這個按鈕之后會調用 openNativePage 來打開一個新的原生頁面。
~~~
void main() => runApp(_widgetForRoute(window.defaultRouteName));
// 獲取方法通道
const platform = MethodChannel('samples.chenhang/navigation');
// 根據路由標識符返回應用入口視圖
Widget _widgetForRoute(String route) {
switch (route) {
default:// 返回默認視圖
return MaterialApp(home:DefaultPage());
}
}
class PageA extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
body: RaisedButton(
child: Text("Go PageB"),
onPressed: ()=>platform.invokeMethod('openNativePage')// 打開原生頁面
));
}
}
class DefaultPage extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("DefaultPage Page"),
leading: IconButton(icon:Icon(Icons.arrow_back), onPressed:() => platform.invokeMethod('closeFlutterPage')// 關閉 Flutter 頁面
)),
body: RaisedButton(
child: Text("Go PageA"),
onPressed: ()=>Navigator.push(context, MaterialPageRoute(builder: (context) => PageA())),// 打開 Flutter 頁面 PageA
));
}
}
~~~
整個混合導航棧示例的代碼流程,如下圖所示。通過這張圖,你就可以把這個示例的整個代碼流程串起來了。
:-: 
圖 3 混合導航棧示例
在我們的混合應用中,RootViewController 與 MainActivity 分別是 iOS 和 Android 應用的原生頁面入口,可以初始化為 Flutter 容器的 FlutterHomeViewController(iOS 端)與 FlutterHomeActivity(Android 端)。
在為其設置初始路由頁面 DefaultPage 之后,就可以以原生的方式跳轉至 Flutter 頁面。但是,Flutter 并未提供接口,來支持從 Flutter 的 DefaultPage 頁面返回到原生頁面,因此我們需要利用方法通道來注冊關閉 Flutter 容器的方法,即 closeFlutterPage,讓 Flutter 容器接收到這個方法調用時關閉自身。
在 Flutter 容器內部,我們可以使用 Flutter 內部的頁面路由機制,通過 Navigator.push 方法,完成從 DefaultPage 到 PageA 的頁面跳轉;而當我們想從 Flutter 的 PageA 頁面跳轉到原生頁面時,因為涉及到跨引擎的頁面路由,所以我們仍然需要利用方法通道來注冊打開原生頁面的方法,即 openNativePage,讓 Flutter 容器接收到這個方法調用時,在原生代碼宿主完成原生頁面 SomeOtherNativeViewController(iOS 端)與 SomeNativePageActivity(Android 端)的初始化,并最終完成頁面跳轉。
## 總結
好了,今天的分享就到這里。我們一起總結下今天的主要內容吧。
對于原生 Android、iOS 工程混編 Flutter 開發,由于應用中會同時存在 Android、iOS 和 Flutter 頁面,所以我們需要妥善處理跨渲染引擎的頁面跳轉,解決原生頁面如何切換 Flutter 頁面,以及 Flutter 頁面如何切換到原生頁面的問題。
在原生頁面切換到 Flutter 頁面時,我們通常會將 Flutter 容器封裝成一個獨立的 ViewController(iOS 端)或 Activity(Android 端),在為其設置好 Flutter 容器的頁面初始化路由(即根視圖)后,原生的代碼就可以按照打開一個普通的原生頁面的方式,來打開 Flutter 頁面了。
而如果我們想在 Flutter 頁面跳轉到原生頁面,則需要同時處理好打開新的原生頁面,以及關閉自身回退到老的原生頁面兩種場景。在這兩種場景下,我們都需要利用方法通道來注冊相應的處理方法,從而在原生代碼宿主實現新頁面的打開和 Flutter 容器的關閉。
需要注意的是,與純 Flutter 應用不同,原生應用混編 Flutter 由于涉及到原生頁面與 Flutter 頁面之間切換,因此導航棧內可能會出現多個 Flutter 容器的情況,即多個 Flutter 實例。
Flutter 實例的初始化成本非常高昂,每啟動一個 Flutter 實例,就會創建一套新的渲染機制,即 Flutter Engine,以及底層的 Isolate。而這些實例之間的內存是不互相共享的,會帶來較大的系統資源消耗。
因此我們在實際業務開發中,應該盡量用 Flutter 去開發閉環的業務模塊,原生只需要能夠跳轉到 Flutter 模塊,剩下的業務都應該在 Flutter 內部完成,而**盡量避免 Flutter 頁面又跳回到原生頁面,原生頁面又啟動新的 Flutter 實例的情況**。
為了解決混編工程中 Flutter 多實例的問題,業界有兩種解決方案:
* 以今日頭條為代表的[修改 Flutter Engine 源碼](https://mp.weixin.qq.com/s/-vyU1JQzdGLUmLGHRImIvg),使多 FlutterView 實例對應的多 Flutter Engine 能夠在底層共享 Isolate;
* 以閑魚為代表的[共享 FlutterView](https://www.infoq.cn/article/VBqfCIuwdjtU_CmcKaEu),即由原生層驅動 Flutter 層渲染內容的方案。
坦白說,這兩種方案各有不足:
* 前者涉及到修改 Flutter 源碼,不僅開發維護成本高,而且增加了線程模型和內存回收出現異常的概率,穩定性不可控。
* 后者涉及到跨渲染引擎的 hack,包括 Flutter 頁面的新建、緩存和內存回收等機制,因此在一些低端機或是處理頁面切換動畫時,容易出現渲染 Bug。
* 除此之外,這兩種方式均與 Flutter 的內部實現綁定較緊,因此在處理 Flutter SDK 版本升級時往往需要耗費較大的適配成本。
綜合來說,目前這兩種解決方案都不夠完美。所以,在 Flutter 官方支持多實例單引擎之前,我們還是盡量在產品模塊層面,保證應用內不要出現多個 Flutter 容器實例吧。
我把今天分享所涉及到的知識點打包到了 GitHub([flutter\_module\_page](https://github.com/cyndibaby905/29_flutter_module_page)、[android\_demo](https://github.com/cyndibaby905/29_android_hybrid_demo)、[iOS\_demo](https://github.com/cyndibaby905/29_ios_hybrid_demo))中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留兩道思考題吧。
1. 請在 openNativePage 方法的基礎上,增加頁面 id 的功能,可以支持在 Flutter 頁面打開任意的原生頁面。
2. 混編工程中會出現兩種頁面過渡動畫:原生頁面之間的切換動畫、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混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略