為了把 Flutter 引入到原生工程,我們需要把 Flutter 工程改造為原生工程的一個組件依賴,并以組件化的方式管理不同平臺的 Flutter 構(gòu)建產(chǎn)物,即 Android 平臺使用 aar、iOS 平臺使用 pod 進(jìn)行依賴管理。這樣,我們就可以在 Android 工程中通過 FlutterView,iOS 工程中通過 FlutterViewController,為 Flutter 搭建應(yīng)用入口,實(shí)現(xiàn) Flutter 與原生的混合開發(fā)方式。
對于混合開發(fā)的應(yīng)用而言,通常我們只會將應(yīng)用的部分模塊修改成 Flutter 開發(fā),其他模塊繼續(xù)保留原生開發(fā),因此應(yīng)用內(nèi)除了 Flutter 的頁面之外,還會有原生 Android、iOS 的頁面。在這種情況下,F(xiàn)lutter 頁面有可能會需要跳轉(zhuǎn)到原生頁面,而原生頁面也可能會需要跳轉(zhuǎn)到 Flutter 頁面。這就涉及到了一個新的問題:如何統(tǒng)一管理原生頁面和 Flutter 頁面跳轉(zhuǎn)交互的混合導(dǎo)航棧。
混合導(dǎo)航棧
混合導(dǎo)航棧,指的是在混合開發(fā)中原生頁面和Flutter頁面相互摻雜,存在于用戶視角的頁面導(dǎo)航棧視圖,如圖11-12所示。在混合開發(fā)的應(yīng)用中,原生Android、iOS與Flutter各自實(shí)現(xiàn)了一套互不相同的頁面映射機(jī)制,原生平臺采用的是單容器單頁面,即一個ViewController或Activity對應(yīng)一個原生頁面;而Flutter采用單容器多頁面的機(jī)制,即一個ViewController或Activity對應(yīng)多個Flutter頁面。Flutter在原生的導(dǎo)航棧之上又自建了一套Flutter導(dǎo)航棧,這使得原生頁面與Flutter頁面與之間進(jìn)行頁面切換時(shí),需要處理跨引擎的頁面切換問題。
接下來,我們就分別從原生頁面跳轉(zhuǎn)至 Flutter 頁面,以及從 Flutter 頁面跳轉(zhuǎn)至原生頁面來看看混合開發(fā)的路由管理。
原生頁面跳轉(zhuǎn)Flutter頁面
從原生頁面跳轉(zhuǎn)至 Flutter 頁面,實(shí)現(xiàn)起來比較簡單。因?yàn)?Flutter 本身依托于原生提供的容器,即iOS 使用的是FlutterViewController,Android 使用的是Activity 中的 FlutterView。所以我們通過初始化 Flutter 容器,為其設(shè)置初始路由頁面之后,就可以以原生的方式跳轉(zhuǎn)至 Flutter 頁面了。
對于iOS混合工程來說,可以先初始化一個FlutterViewController實(shí)例,然后設(shè)置初始化頁面路由,將其加入原生的視圖導(dǎo)航棧中即可完成跳轉(zhuǎn),如下所示。
//iOS 跳轉(zhuǎn)至Flutter頁面
FlutterViewController *vc = [[FlutterViewController alloc] init];
//設(shè)置Flutter初始化路由頁面
[vc setInitialRoute:@"defaultPage"];
//完成頁面跳轉(zhuǎn)
[self.navigationController pushViewController:vc animated:YES];
對于Android混合工程而言,則需要多加一步。因?yàn)镕lutter頁面的入口并不是原生視圖導(dǎo)航棧的最小單位Activity,而是一個FlutterView,所以我們需要把這個View包裝到Activity的contentView中,然后才能實(shí)現(xiàn)跳轉(zhuǎn)。在Activity內(nèi)部設(shè)置頁面初始化路由之后,在外部就可以采用打開一個普通的原生視圖的方式來打開Flutter頁面了,如下所示。
//Android 跳轉(zhuǎn)至Flutter頁面
//創(chuàng)建一個作為Flutter頁面容器的Activity
public class FlutterHomeActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設(shè)置Flutter初始化路由頁面,傳入路由標(biāo)識符
View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute");
//用FlutterView替代Activity的ContentView
setContentView(FlutterView);
}
}
//用FlutterPageActivity完成頁面跳轉(zhuǎn)
Intent intent = new Intent(MainActivity.this, FlutterHomeActivity.class);
startActivity(intent);
運(yùn)行項(xiàng)目代碼,最終的效果下圖所示。
對于Android混合工程來說,F(xiàn)lutter的原生容器就是一個Activity,只需要創(chuàng)建一個FlutterView,然后利用addContentView()方法將當(dāng)前頁面的layout頁面布局添加進(jìn)去即可。如果Flutter的原生容器是一個Fragment,那么只需要創(chuàng)建一個FlutterFragment,然后在指定的容器中添加Flutter頁面即可。同樣,對于iOS混合工程來說,F(xiàn)lutter的原生容器是一個FlutterViewController。
Flutter 頁面跳轉(zhuǎn)至原生頁面
相比原生頁面跳轉(zhuǎn)Flutter頁面,從Flutter頁面跳轉(zhuǎn)至原生頁面則會相對麻煩些。因?yàn)槲覀冃枰紤]以下兩種場景,即從Flutter頁面打開新的原生頁面和從Flutter頁面回退到舊的原生頁面。
由于Flutter并沒有提供對原生頁面的操作方法,所以不能通過直接調(diào)用原生平臺的方法來實(shí)現(xiàn)頁面跳轉(zhuǎn),不過可以使用Flutter提供的方法通道來間接實(shí)現(xiàn),即打開原生頁面使用的是openNativePage()方法,需要關(guān)閉Flutter頁面時(shí)則調(diào)用closeFlutterPage()方法。
具體來說,在Flutter和原生兩端各自初始化方法通道,并提供Flutter操作原生頁面的方法,并在原生代碼中注冊方法通道,當(dāng)原生端收到Flutter的方法調(diào)用時(shí)就可以打開新的原生頁面。
在混合開發(fā)的應(yīng)用中,F(xiàn)lutterView與FlutterViewController是Flutter模塊的入口,也是Flutter模塊初始化的地方。可以看到,在混合開發(fā)的應(yīng)用中接入Flutter與開發(fā)一個純Flutter應(yīng)用在運(yùn)行機(jī)制上并無任何區(qū)別,因?yàn)閷τ诨旌瞎こ虂碚f,原生工程只不過是為Flutter提供了一個容器而已,即Android使用的是FlutterView,iOS使用的是FlutterViewController。接下來,F(xiàn)lutter模塊就可以使用自己的導(dǎo)航棧來管理Flutter頁面,并且可以實(shí)現(xiàn)多個復(fù)雜頁面的渲染和切換。
因?yàn)镕lutter容器本身屬于原生導(dǎo)航棧的一部分,所以當(dāng)Flutter容器內(nèi)的根頁面需要返回時(shí),開發(fā)者需要處理Flutter容器的關(guān)閉問題,從而實(shí)現(xiàn)Flutter根頁面的關(guān)閉。由于Flutter并沒有提供操作Flutter容器的方法,因此我們依然需要通過方法通道,在原生代碼宿主為Flutter提供操作Flutter容器的方法,在頁面返回時(shí)關(guān)閉Flutter頁面。如圖下圖所示,是Flutter跳轉(zhuǎn)原生頁面的兩種場景的示意圖。
使用方法通道實(shí)現(xiàn)Flutter頁面至原生頁面的跳轉(zhuǎn),注冊方法通道最合適的地方是Flutter應(yīng)用的入口,即在iOS端的FlutterViewController和Android端的是FlutterView初始化Flutter頁面之前。因此,在混合開發(fā)的應(yīng)用中,需要分別繼承iOS的FlutterViewController和Android的AppCompatActivity,然后在iOS的viewDidLoad和Android的onCreate生命周期函數(shù)中初始化Flutter容器時(shí),注冊openNativePage和closeFlutterPage兩個方法。
下面是使用方法通道實(shí)現(xiàn)Flutter跳轉(zhuǎn)原生頁面的原生Android端的代碼,如下所示。
public class FlutterModuleActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化Flutter容器
FlutterView fv = Flutter.createView(this, getLifecycle(), "defaultPage");
//注冊方法通道
new MethodChannel(fv, "com.xzh/navigation").setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("openNativePage")) {
Intent intent = new Intent(this, AndroidNativeActivity.class);
tartActivity(intent);
result.success(0);
} else if (call.method.equals("closeFlutterPage")) {
finish();
result.success(0);
} else {
result.notImplemented();
}
}
});
setContentView(fv);
}
}
可以發(fā)現(xiàn),在上面的代碼中,首先使用FlutterView初始化一個Flutter容器,然后在原生代碼中注冊openNativePage和closeFlutterPage兩個方法,當(dāng)Flutter頁面通過方法通道調(diào)用原生方法時(shí)即可打開原生頁面。
與原生Android端的實(shí)現(xiàn)原理類似,使用方法通道實(shí)現(xiàn)頁面的跳轉(zhuǎn)頁需要在原生iOS端中注冊openNativePage和closeFlutterPage兩個方法,代碼如下。
@interface FlutterHomeViewController : FlutterViewController
@end
@implementation FlutterHomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
//聲明方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"com.xzh/navigation" binaryMessenger:self];
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
if([call.method isEqualToString:@"openNativePage"]) {
//打開一個新的原生頁面
iOSNativeViewController *vc = [[iOSNativeViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
result(@0);
}else if([call.method isEqualToString:@"closeFlutterPage"]) {
//關(guān)閉Flutter頁面
[self.navigationController popViewControllerAnimated:YES];
result(@0);
}else {
result(FlutterMethodNotImplemented);
}
}];
}
@end
經(jīng)過上面的方法注冊后,接下來就可以在Flutter中使用openNativePage()方法來打開原生頁面了,如下所示。
void main() => runApp(_widgetForRoute(window.defaultRouteName));
//獲取方法通道
const platform = MethodChannel('com.xzh/navigation');
//根據(jù)路由標(biāo)識符返回應(yīng)用入口視圖
Widget _widgetForRoute(String route) {
switch (route) {
default://返回默認(rèn)視圖
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')//關(guān)閉Flutter頁面
)),
body: RaisedButton(
child: Text("Go PageA"),
onPressed: ()=>Navigator.push(context, MaterialPageRoute(builder: (context) => PageA())),//打開Flutter頁面 PageA
));
}
}
在上面的例子中,F(xiàn)lutter 容器的根視圖 DefaultPage 包含有兩個按鈕。點(diǎn)擊左上角的按鈕后,可以通過 closeFlutterPage 返回原生頁面;點(diǎn)擊中間的按鈕后,會打開一個新的 Flutter 頁面 PageA。PageA 中也有一個按鈕,點(diǎn)擊這個按鈕之后會調(diào)用 openNativePage 來打開一個新的原生頁面。
整個混合導(dǎo)航棧示例的代碼流程,如下圖所示。通過這張圖,你就可以把這個示例的整個代碼流程串起來了。
在混合應(yīng)用工程中,RootViewController 與 MainActivity 分別是 iOS 和 Android 應(yīng)用的原生頁面入口,可以初始化為 Flutter 容器的 FlutterHomeViewController(iOS 端)與 FlutterHomeActivity(Android 端)。
在為其設(shè)置初始路由頁面 DefaultPage 之后,就可以以原生的方式跳轉(zhuǎn)至 Flutter 頁面。但是,F(xiàn)lutter 并未提供接口,來支持從 Flutter 的 DefaultPage 頁面返回到原生頁面,因此我們需要利用方法通道來注冊關(guān)閉 Flutter 容器的方法,即 closeFlutterPage,讓 Flutter 容器接收到這個方法調(diào)用時(shí)關(guān)閉自身。
在 Flutter 容器內(nèi)部,我們可以使用 Flutter 內(nèi)部的頁面路由機(jī)制,通過 Navigator.push 方法,完成從 DefaultPage 到 PageA 的頁面跳轉(zhuǎn);而當(dāng)我們想從 Flutter 的 PageA 頁面跳轉(zhuǎn)到原生頁面時(shí),因?yàn)樯婕暗娇缫娴捻撁媛酚桑晕覀內(nèi)匀恍枰梅椒ㄍǖ纴碜源蜷_原生頁面的方法,即 openNativePage,讓 Flutter 容器接收到這個方法調(diào)用時(shí),在原生代碼宿主完成原生頁面 SomeOtherNativeViewController(iOS 端)與 SomeNativePageActivity(Android 端)的初始化,并最終完成頁面跳轉(zhuǎn)。
總結(jié)
對于原生 Android、iOS 工程混編 Flutter 開發(fā),由于應(yīng)用中會同時(shí)存在 Android、iOS 和 Flutter 頁面,所以我們需要妥善處理跨渲染引擎的頁面跳轉(zhuǎn),解決原生頁面如何切換 Flutter 頁面,以及 Flutter 頁面如何切換到原生頁面的問題。
在原生頁面切換到 Flutter 頁面時(shí),我們通常會將 Flutter 容器封裝成一個獨(dú)立的 ViewController(iOS 端)或 Activity(Android 端),在為其設(shè)置好 Flutter 容器的頁面初始化路由(即根視圖)后,原生的代碼就可以按照打開一個普通的原生頁面的方式來打開 Flutter 頁面了。
而如果我們想在 Flutter 頁面跳轉(zhuǎn)到原生頁面,則需要同時(shí)處理好打開新的原生頁面,以及關(guān)閉自身回退到老的原生頁面兩種場景。在這兩種場景下,我們都需要利用方法通道來注冊相應(yīng)的處理方法,從而在原生代碼宿主實(shí)現(xiàn)新頁面的打開和 Flutter 容器的關(guān)閉。
需要注意的是,與純 Flutter 應(yīng)用不同,原生應(yīng)用混編 Flutter 由于涉及到原生頁面與 Flutter 頁面之間切換,因此導(dǎo)航棧內(nèi)可能會出現(xiàn)多個 Flutter 容器的情況,即多個 Flutter 實(shí)例。Flutter 實(shí)例的初始化成本非常高昂,每啟動一個 Flutter 實(shí)例,就會創(chuàng)建一套新的渲染機(jī)制,即 Flutter Engine,以及底層的 Isolate。而這些實(shí)例之間的內(nèi)存是不互相共享的,會帶來較大的系統(tǒng)資源消耗。
為了解決混編工程中 Flutter 多實(shí)例的問題,業(yè)界有兩種解決方案:
- 以今日頭條為代表的修改 Flutter Engine 源碼,使多 FlutterView 實(shí)例對應(yīng)的多 Flutter Engine 能夠在底層共享 Isolate;
- 以閑魚為代表的共享 FlutterView,即由原生層驅(qū)動 Flutter 層渲染內(nèi)容的方案。
不過,目前這兩種解決方案都不夠完美。所以,在 Flutter 官方支持多實(shí)例單引擎之前,應(yīng)該盡量使用Flutter去開發(fā)一些閉環(huán)業(yè)務(wù),減少原生頁面與Flutter頁面之間的交互,盡量避免Flutter頁面跳轉(zhuǎn)到原生頁面,原生頁面又啟動一個新的Flutter實(shí)例的情況,并且保證應(yīng)用內(nèi)不要出現(xiàn)多個 Flutter 容器實(shí)例的情況。
原文作者:xiangzhihong
原文鏈接:人類身份驗(yàn)證 - SegmentFault
來源:思否