為了把 Flutter 引入到原生工程炒辉,我們需要把 Flutter 工程改造為原生工程的一個(gè)組件依賴豪墅,并以組件化的方式管理不同平臺(tái)的 Flutter 構(gòu)建產(chǎn)物,即 Android 平臺(tái)使用 aar黔寇、iOS 平臺(tái)使用 pod 進(jìn)行依賴管理偶器。這樣,我們就可以在 Android 工程中通過(guò) FlutterView缝裤,iOS 工程中通過(guò) FlutterViewController屏轰,為 Flutter 搭建應(yīng)用入口,實(shí)現(xiàn) Flutter 與原生的混合開(kāi)發(fā)方式倘是。
對(duì)于混合開(kāi)發(fā)的應(yīng)用而言亭枷,通常我們只會(huì)將應(yīng)用的部分模塊修改成 Flutter 開(kāi)發(fā),其他模塊繼續(xù)保留原生開(kāi)發(fā)搀崭,因此應(yīng)用內(nèi)除了 Flutter 的頁(yè)面之外叨粘,還會(huì)有原生 Android、iOS 的頁(yè)面瘤睹。在這種情況下升敲,F(xiàn)lutter 頁(yè)面有可能會(huì)需要跳轉(zhuǎn)到原生頁(yè)面,而原生頁(yè)面也可能會(huì)需要跳轉(zhuǎn)到 Flutter 頁(yè)面轰传。這就涉及到了一個(gè)新的問(wèn)題:如何統(tǒng)一管理原生頁(yè)面和 Flutter 頁(yè)面跳轉(zhuǎn)交互的混合導(dǎo)航棧驴党。
混合導(dǎo)航棧
混合導(dǎo)航棧,指的是在混合開(kāi)發(fā)中原生頁(yè)面和Flutter頁(yè)面相互摻雜获茬,存在于用戶視角的頁(yè)面導(dǎo)航棧視圖港庄,如圖11-12所示。在混合開(kāi)發(fā)的應(yīng)用中恕曲,原生Android鹏氧、iOS與Flutter各自實(shí)現(xiàn)了一套互不相同的頁(yè)面映射機(jī)制,原生平臺(tái)采用的是單容器單頁(yè)面佩谣,即一個(gè)ViewController或Activity對(duì)應(yīng)一個(gè)原生頁(yè)面把还;而Flutter采用單容器多頁(yè)面的機(jī)制,即一個(gè)ViewController或Activity對(duì)應(yīng)多個(gè)Flutter頁(yè)面茸俭。Flutter在原生的導(dǎo)航棧之上又自建了一套Flutter導(dǎo)航棧吊履,這使得原生頁(yè)面與Flutter頁(yè)面與之間進(jìn)行頁(yè)面切換時(shí),需要處理跨引擎的頁(yè)面切換問(wèn)題调鬓。
接下來(lái)艇炎,我們就分別從原生頁(yè)面跳轉(zhuǎn)至 Flutter 頁(yè)面,以及從 Flutter 頁(yè)面跳轉(zhuǎn)至原生頁(yè)面來(lái)看看混合開(kāi)發(fā)的路由管理腾窝。
原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面
從原生頁(yè)面跳轉(zhuǎn)至 Flutter 頁(yè)面冕臭,實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單腺晾。因?yàn)?Flutter 本身依托于原生提供的容器,即iOS 使用的是FlutterViewController辜贵,Android 使用的是Activity 中的 FlutterView悯蝉。所以我們通過(guò)初始化 Flutter 容器,為其設(shè)置初始路由頁(yè)面之后托慨,就可以以原生的方式跳轉(zhuǎn)至 Flutter 頁(yè)面了鼻由。
對(duì)于iOS混合工程來(lái)說(shuō),可以先初始化一個(gè)FlutterViewController實(shí)例厚棵,然后設(shè)置初始化頁(yè)面路由蕉世,將其加入原生的視圖導(dǎo)航棧中即可完成跳轉(zhuǎn),如下所示婆硬。
//iOS 跳轉(zhuǎn)至Flutter頁(yè)面
FlutterViewController *vc = [[FlutterViewController alloc] init];
//設(shè)置Flutter初始化路由頁(yè)面
[vc setInitialRoute:@"defaultPage"];
//完成頁(yè)面跳轉(zhuǎn)
[self.navigationController pushViewController:vc animated:YES];
對(duì)于Android混合工程而言狠轻,則需要多加一步。因?yàn)镕lutter頁(yè)面的入口并不是原生視圖導(dǎo)航棧的最小單位Activity彬犯,而是一個(gè)FlutterView向楼,所以我們需要把這個(gè)View包裝到Activity的contentView中,然后才能實(shí)現(xiàn)跳轉(zhuǎn)谐区。在Activity內(nèi)部設(shè)置頁(yè)面初始化路由之后湖蜕,在外部就可以采用打開(kāi)一個(gè)普通的原生視圖的方式來(lái)打開(kāi)Flutter頁(yè)面了,如下所示宋列。
//Android 跳轉(zhuǎn)至Flutter頁(yè)面
//創(chuàng)建一個(gè)作為Flutter頁(yè)面容器的Activity
public class FlutterHomeActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設(shè)置Flutter初始化路由頁(yè)面昭抒,傳入路由標(biāo)識(shí)符
View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute");
//用FlutterView替代Activity的ContentView
setContentView(FlutterView);
}
}
//用FlutterPageActivity完成頁(yè)面跳轉(zhuǎn)
Intent intent = new Intent(MainActivity.this, FlutterHomeActivity.class);
startActivity(intent);
運(yùn)行項(xiàng)目代碼,最終的效果下圖所示炼杖。
對(duì)于Android混合工程來(lái)說(shuō)灭返,F(xiàn)lutter的原生容器就是一個(gè)Activity,只需要?jiǎng)?chuàng)建一個(gè)FlutterView坤邪,然后利用addContentView()方法將當(dāng)前頁(yè)面的layout頁(yè)面布局添加進(jìn)去即可熙含。如果Flutter的原生容器是一個(gè)Fragment,那么只需要?jiǎng)?chuàng)建一個(gè)FlutterFragment罩扇,然后在指定的容器中添加Flutter頁(yè)面即可婆芦。同樣怕磨,對(duì)于iOS混合工程來(lái)說(shuō)喂饥,F(xiàn)lutter的原生容器是一個(gè)FlutterViewController。
Flutter 頁(yè)面跳轉(zhuǎn)至原生頁(yè)面
相比原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面肠鲫,從Flutter頁(yè)面跳轉(zhuǎn)至原生頁(yè)面則會(huì)相對(duì)麻煩些员帮。因?yàn)槲覀冃枰紤]以下兩種場(chǎng)景,即從Flutter頁(yè)面打開(kāi)新的原生頁(yè)面和從Flutter頁(yè)面回退到舊的原生頁(yè)面导饲。
由于Flutter并沒(méi)有提供對(duì)原生頁(yè)面的操作方法捞高,所以不能通過(guò)直接調(diào)用原生平臺(tái)的方法來(lái)實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)氯材,不過(guò)可以使用Flutter提供的方法通道來(lái)間接實(shí)現(xiàn),即打開(kāi)原生頁(yè)面使用的是openNativePage()方法硝岗,需要關(guān)閉Flutter頁(yè)面時(shí)則調(diào)用closeFlutterPage()方法氢哮。
具體來(lái)說(shuō),在Flutter和原生兩端各自初始化方法通道型檀,并提供Flutter操作原生頁(yè)面的方法冗尤,并在原生代碼中注冊(cè)方法通道,當(dāng)原生端收到Flutter的方法調(diào)用時(shí)就可以打開(kāi)新的原生頁(yè)面胀溺。
在混合開(kāi)發(fā)的應(yīng)用中裂七,F(xiàn)lutterView與FlutterViewController是Flutter模塊的入口,也是Flutter模塊初始化的地方仓坞”沉悖可以看到,在混合開(kāi)發(fā)的應(yīng)用中接入Flutter與開(kāi)發(fā)一個(gè)純Flutter應(yīng)用在運(yùn)行機(jī)制上并無(wú)任何區(qū)別无埃,因?yàn)閷?duì)于混合工程來(lái)說(shuō)徙瓶,原生工程只不過(guò)是為Flutter提供了一個(gè)容器而已,即Android使用的是FlutterView录语,iOS使用的是FlutterViewController倍啥。接下來(lái),F(xiàn)lutter模塊就可以使用自己的導(dǎo)航棧來(lái)管理Flutter頁(yè)面澎埠,并且可以實(shí)現(xiàn)多個(gè)復(fù)雜頁(yè)面的渲染和切換虽缕。
因?yàn)镕lutter容器本身屬于原生導(dǎo)航棧的一部分,所以當(dāng)Flutter容器內(nèi)的根頁(yè)面需要返回時(shí)蒲稳,開(kāi)發(fā)者需要處理Flutter容器的關(guān)閉問(wèn)題氮趋,從而實(shí)現(xiàn)Flutter根頁(yè)面的關(guān)閉。由于Flutter并沒(méi)有提供操作Flutter容器的方法江耀,因此我們依然需要通過(guò)方法通道剩胁,在原生代碼宿主為Flutter提供操作Flutter容器的方法,在頁(yè)面返回時(shí)關(guān)閉Flutter頁(yè)面祥国。如圖下圖所示昵观,是Flutter跳轉(zhuǎn)原生頁(yè)面的兩種場(chǎng)景的示意圖。
使用方法通道實(shí)現(xiàn)Flutter頁(yè)面至原生頁(yè)面的跳轉(zhuǎn)舌稀,注冊(cè)方法通道最合適的地方是Flutter應(yīng)用的入口啊犬,即在iOS端的FlutterViewController和Android端的是FlutterView初始化Flutter頁(yè)面之前。因此壁查,在混合開(kāi)發(fā)的應(yīng)用中觉至,需要分別繼承iOS的FlutterViewController和Android的AppCompatActivity,然后在iOS的viewDidLoad和Android的onCreate生命周期函數(shù)中初始化Flutter容器時(shí)睡腿,注冊(cè)openNativePage和closeFlutterPage兩個(gè)方法语御。
下面是使用方法通道實(shí)現(xiàn)Flutter跳轉(zhuǎn)原生頁(yè)面的原生Android端的代碼峻贮,如下所示。
public class FlutterModuleActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化Flutter容器
FlutterView fv = Flutter.createView(this, getLifecycle(), "defaultPage");
//注冊(cè)方法通道
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初始化一個(gè)Flutter容器,然后在原生代碼中注冊(cè)openNativePage和closeFlutterPage兩個(gè)方法碉纺,當(dāng)Flutter頁(yè)面通過(guò)方法通道調(diào)用原生方法時(shí)即可打開(kāi)原生頁(yè)面嚼黔。
與原生Android端的實(shí)現(xiàn)原理類(lèi)似,使用方法通道實(shí)現(xiàn)頁(yè)面的跳轉(zhuǎn)頁(yè)需要在原生iOS端中注冊(cè)openNativePage和closeFlutterPage兩個(gè)方法惜辑,代碼如下唬涧。
@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"]) {
//打開(kāi)一個(gè)新的原生頁(yè)面
iOSNativeViewController *vc = [[iOSNativeViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
result(@0);
}else if([call.method isEqualToString:@"closeFlutterPage"]) {
//關(guān)閉Flutter頁(yè)面
[self.navigationController popViewControllerAnimated:YES];
result(@0);
}else {
result(FlutterMethodNotImplemented);
}
}];
}
@end
經(jīng)過(guò)上面的方法注冊(cè)后,接下來(lái)就可以在Flutter中使用openNativePage()方法來(lái)打開(kāi)原生頁(yè)面了盛撑,如下所示碎节。
void main() => runApp(_widgetForRoute(window.defaultRouteName));
//獲取方法通道
const platform = MethodChannel('com.xzh/navigation');
//根據(jù)路由標(biāo)識(shí)符返回應(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')//打開(kāi)原生頁(yè)面
));
}
}
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頁(yè)面
)),
body: RaisedButton(
child: Text("Go PageA"),
onPressed: ()=>Navigator.push(context, MaterialPageRoute(builder: (context) => PageA())),//打開(kāi)Flutter頁(yè)面 PageA
));
}
}
在上面的例子中,F(xiàn)lutter 容器的根視圖 DefaultPage 包含有兩個(gè)按鈕抵卫。點(diǎn)擊左上角的按鈕后狮荔,可以通過(guò) closeFlutterPage 返回原生頁(yè)面;點(diǎn)擊中間的按鈕后介粘,會(huì)打開(kāi)一個(gè)新的 Flutter 頁(yè)面 PageA殖氏。PageA 中也有一個(gè)按鈕,點(diǎn)擊這個(gè)按鈕之后會(huì)調(diào)用 openNativePage 來(lái)打開(kāi)一個(gè)新的原生頁(yè)面姻采。
整個(gè)混合導(dǎo)航棧示例的代碼流程雅采,如下圖所示。通過(guò)這張圖慨亲,你就可以把這個(gè)示例的整個(gè)代碼流程串起來(lái)了婚瓜。
在混合應(yīng)用工程中,RootViewController 與 MainActivity 分別是 iOS 和 Android 應(yīng)用的原生頁(yè)面入口刑棵,可以初始化為 Flutter 容器的 FlutterHomeViewController(iOS 端)與 FlutterHomeActivity(Android 端)巴刻。
在為其設(shè)置初始路由頁(yè)面 DefaultPage 之后,就可以以原生的方式跳轉(zhuǎn)至 Flutter 頁(yè)面蛉签。但是胡陪,F(xiàn)lutter 并未提供接口,來(lái)支持從 Flutter 的 DefaultPage 頁(yè)面返回到原生頁(yè)面碍舍,因此我們需要利用方法通道來(lái)注冊(cè)關(guān)閉 Flutter 容器的方法柠座,即 closeFlutterPage,讓 Flutter 容器接收到這個(gè)方法調(diào)用時(shí)關(guān)閉自身乒验。
在 Flutter 容器內(nèi)部愚隧,我們可以使用 Flutter 內(nèi)部的頁(yè)面路由機(jī)制蒂阱,通過(guò) Navigator.push 方法锻全,完成從 DefaultPage 到 PageA 的頁(yè)面跳轉(zhuǎn)狂塘;而當(dāng)我們想從 Flutter 的 PageA 頁(yè)面跳轉(zhuǎn)到原生頁(yè)面時(shí),因?yàn)樯婕暗娇缫娴捻?yè)面路由鳄厌,所以我們?nèi)匀恍枰梅椒ㄍǖ纴?lái)注冊(cè)打開(kāi)原生頁(yè)面的方法荞胡,即 openNativePage,讓 Flutter 容器接收到這個(gè)方法調(diào)用時(shí)了嚎,在原生代碼宿主完成原生頁(yè)面 SomeOtherNativeViewController(iOS 端)與 SomeNativePageActivity(Android 端)的初始化泪漂,并最終完成頁(yè)面跳轉(zhuǎn)。
總結(jié)
對(duì)于原生 Android歪泳、iOS 工程混編 Flutter 開(kāi)發(fā)萝勤,由于應(yīng)用中會(huì)同時(shí)存在 Android、iOS 和 Flutter 頁(yè)面呐伞,所以我們需要妥善處理跨渲染引擎的頁(yè)面跳轉(zhuǎn)敌卓,解決原生頁(yè)面如何切換 Flutter 頁(yè)面,以及 Flutter 頁(yè)面如何切換到原生頁(yè)面的問(wèn)題伶氢。
在原生頁(yè)面切換到 Flutter 頁(yè)面時(shí)趟径,我們通常會(huì)將 Flutter 容器封裝成一個(gè)獨(dú)立的 ViewController(iOS 端)或 Activity(Android 端),在為其設(shè)置好 Flutter 容器的頁(yè)面初始化路由(即根視圖)后癣防,原生的代碼就可以按照打開(kāi)一個(gè)普通的原生頁(yè)面的方式來(lái)打開(kāi) Flutter 頁(yè)面了蜗巧。
而如果我們想在 Flutter 頁(yè)面跳轉(zhuǎn)到原生頁(yè)面,則需要同時(shí)處理好打開(kāi)新的原生頁(yè)面蕾盯,以及關(guān)閉自身回退到老的原生頁(yè)面兩種場(chǎng)景幕屹。在這兩種場(chǎng)景下,我們都需要利用方法通道來(lái)注冊(cè)相應(yīng)的處理方法级遭,從而在原生代碼宿主實(shí)現(xiàn)新頁(yè)面的打開(kāi)和 Flutter 容器的關(guān)閉香嗓。
需要注意的是,與純 Flutter 應(yīng)用不同装畅,原生應(yīng)用混編 Flutter 由于涉及到原生頁(yè)面與 Flutter 頁(yè)面之間切換靠娱,因此導(dǎo)航棧內(nèi)可能會(huì)出現(xiàn)多個(gè) Flutter 容器的情況,即多個(gè) Flutter 實(shí)例掠兄。Flutter 實(shí)例的初始化成本非常高昂像云,每啟動(dòng)一個(gè) Flutter 實(shí)例,就會(huì)創(chuàng)建一套新的渲染機(jī)制蚂夕,即 Flutter Engine迅诬,以及底層的 Isolate。而這些實(shí)例之間的內(nèi)存是不互相共享的婿牍,會(huì)帶來(lái)較大的系統(tǒng)資源消耗侈贷。
為了解決混編工程中 Flutter 多實(shí)例的問(wèn)題,業(yè)界有兩種解決方案:
- 以今日頭條為代表的修改 Flutter Engine 源碼等脂,使多 FlutterView 實(shí)例對(duì)應(yīng)的多 Flutter Engine 能夠在底層共享 Isolate俏蛮;
- 以閑魚(yú)為代表的共享 FlutterView撑蚌,即由原生層驅(qū)動(dòng) Flutter 層渲染內(nèi)容的方案。
不過(guò)搏屑,目前這兩種解決方案都不夠完美争涌。所以,在 Flutter 官方支持多實(shí)例單引擎之前辣恋,應(yīng)該盡量使用Flutter去開(kāi)發(fā)一些閉環(huán)業(yè)務(wù)亮垫,減少原生頁(yè)面與Flutter頁(yè)面之間的交互,盡量避免Flutter頁(yè)面跳轉(zhuǎn)到原生頁(yè)面伟骨,原生頁(yè)面又啟動(dòng)一個(gè)新的Flutter實(shí)例的情況饮潦,并且保證應(yīng)用內(nèi)不要出現(xiàn)多個(gè) Flutter 容器實(shí)例的情況。
原文作者:xiangzhihong
原文鏈接:人類(lèi)身份驗(yàn)證 - SegmentFault
來(lái)源:思否