爲了把 Flutter 引入到原生工程,咱們須要把 Flutter 工程改造爲原生工程的一個組件依賴,並以組件化的方式管理不一樣平臺的 Flutter 構建產物,即 Android 平臺使用 aar、iOS 平臺使用 pod 進行依賴管理。這樣,咱們就能夠在 Android 工程中經過 FlutterView,iOS 工程中經過 FlutterViewController,爲 Flutter 搭建應用入口,實現 Flutter 與原生的混合開發方式。bash
對於混合開發的應用而言,一般咱們只會將應用的部分模塊修改爲 Flutter 開發,其餘模塊繼續保留原生開發,所以應用內除了 Flutter 的頁面以外,還會有原生 Android、iOS 的頁面。在這種狀況下,Flutter 頁面有可能會須要跳轉到原生頁面,而原生頁面也可能會須要跳轉到 Flutter 頁面。這就涉及到了一個新的問題:如何統一管理原生頁面和 Flutter 頁面跳轉交互的混合導航棧。app
混合導航棧,指的是在混合開發中原生頁面和Flutter頁面相互摻雜,存在於用戶視角的頁面導航棧視圖,如圖11-12所示。在混合開發的應用中,原生Android、iOS與Flutter各自實現了一套互不相同的頁面映射機制,原平生臺採用的是單容器單頁面,即一個ViewController或Activity對應一個原生頁面;而Flutter採用單容器多頁面的機制,即一個ViewController或Activity對應多個Flutter頁面。Flutter在原生的導航棧之上又自建了一套Flutter導航棧,這使得原生頁面與Flutter頁面與之間進行頁面切換時,須要處理跨引擎的頁面切換問題。less
接下來,咱們就分別從原生頁面跳轉至 Flutter 頁面,以及從 Flutter 頁面跳轉至原生頁面來看看混合開發的路由管理。從原生頁面跳轉至 Flutter 頁面,實現起來比較簡單。由於 Flutter 自己依託於原生提供的容器,即iOS 使用的是FlutterViewController,Android 使用的是Activity 中的 FlutterView。因此咱們經過初始化 Flutter 容器,爲其設置初始路由頁面以後,就能夠以原生的方式跳轉至 Flutter 頁面了。ide
對於iOS混合工程來講,能夠先初始化一個FlutterViewController實例,而後設置初始化頁面路由,將其加入原生的視圖導航棧中便可完成跳轉,以下所示。函數
//iOS 跳轉至Flutter頁面
FlutterViewController *vc = [[FlutterViewController alloc] init];
//設置Flutter初始化路由頁面
[vc setInitialRoute:@"defaultPage"];
//完成頁面跳轉
[self.navigationController pushViewController:vc animated:YES];
複製代碼
對於Android混合工程而言,則須要多加一步。由於Flutter頁面的入口並非原生視圖導航棧的最小單位Activity,而是一個FlutterView,因此咱們須要把這個View包裝到Activity的contentView中,而後才能實現跳轉。在Activity內部設置頁面初始化路由以後,在外部就能夠採用打開一個普通的原生視圖的方式來打開Flutter頁面了,以下所示。組件化
//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");
//用FlutterView替代Activity的ContentView
setContentView(FlutterView);
}
}
//用FlutterPageActivity完成頁面跳轉
Intent intent = new Intent(MainActivity.this, FlutterHomeActivity.class);
startActivity(intent);
複製代碼
運行項目代碼,最終的效果下圖所示。 佈局
對於Android混合工程來講,Flutter的原生容器就是一個Activity,只須要建立一個FlutterView,而後利用addContentView()方法將當前頁面的layout頁面佈局添加進去便可。若是Flutter的原生容器是一個Fragment,那麼只須要建立一個FlutterFragment,而後在指定的容器中添加Flutter頁面便可。一樣,對於iOS混合工程來講,Flutter的原生容器是一個FlutterViewController。相比原生頁面跳轉Flutter頁面,從Flutter頁面跳轉至原生頁面則會相對麻煩些。由於咱們須要考慮如下兩種場景,即從Flutter頁面打開新的原生頁面和從Flutter頁面回退到舊的原生頁面。 因爲Flutter並無提供對原生頁面的操做方法,因此不能經過直接調用原平生臺的方法來實現頁面跳轉,不過可使用Flutter提供的方法通道來間接實現,即打開原生頁面使用的是openNativePage()方法,須要關閉Flutter頁面時則調用closeFlutterPage()方法。 具體來講,在Flutter和原生兩端各自初始化方法通道,並提供Flutter操做原生頁面的方法,並在原生代碼中註冊方法通道,當原生端收到Flutter的方法調用時就能夠打開新的原生頁面。 在混合開發的應用中,FlutterView與FlutterViewController是Flutter模塊的入口,也是Flutter模塊初始化的地方。能夠看到,在混合開發的應用中接入Flutter與開發一個純Flutter應用在運行機制上並沒有任何區別,由於對於混合工程來講,原生工程只不過是爲Flutter提供了一個容器而已,即Android使用的是FlutterView,iOS使用的是FlutterViewController。接下來,Flutter模塊就可使用本身的導航棧來管理Flutter頁面,而且能夠實現多個複雜頁面的渲染和切換。 由於Flutter容器自己屬於原生導航棧的一部分,因此當Flutter容器內的根頁面須要返回時,開發者須要處理Flutter容器的關閉問題,從而實現Flutter根頁面的關閉。因爲Flutter並無提供操做Flutter容器的方法,所以咱們依然須要經過方法通道,在原生代碼宿主爲Flutter提供操做Flutter容器的方法,在頁面返回時關閉Flutter頁面。如圖下圖所示,是Flutter跳轉原生頁面的兩種場景的示意圖。 ui
使用方法通道實現Flutter頁面至原生頁面的跳轉,註冊方法通道最合適的地方是Flutter應用的入口,即在iOS端的FlutterViewController和Android端的是FlutterView初始化Flutter頁面以前。所以,在混合開發的應用中,須要分別繼承iOS的FlutterViewController和Android的AppCompatActivity,而後在iOS的viewDidLoad和Android的onCreate生命週期函數中初始化Flutter容器時,註冊openNativePage和closeFlutterPage兩個方法。 下面是使用方法通道實現Flutter跳轉原生頁面的原生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);
}
}
複製代碼
能夠發現,在上面的代碼中,首先使用FlutterView初始化一個Flutter容器,而後在原生代碼中註冊openNativePage和closeFlutterPage兩個方法,當Flutter頁面經過方法通道調用原生方法時便可打開原生頁面。 與原生Android端的實現原理相似,使用方法通道實現頁面的跳轉頁須要在原生iOS端中註冊openNativePage和closeFlutterPage兩個方法,代碼以下。this
@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"]) {
//關閉Flutter頁面
[self.navigationController popViewControllerAnimated:YES];
result(@0);
}else {
result(FlutterMethodNotImplemented);
}
}];
}
@end
複製代碼
通過上面的方法註冊後,接下來就能夠在Flutter中使用openNativePage()方法來打開原生頁面了,以下所示。spa
void main() => runApp(_widgetForRoute(window.defaultRouteName));
//獲取方法通道
const platform = MethodChannel('com.xzh/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
));
}
}
複製代碼
在上面的例子中,Flutter 容器的根視圖 DefaultPage 包含有兩個按鈕。點擊左上角的按鈕後,能夠經過 closeFlutterPage 返回原生頁面;點擊中間的按鈕後,會打開一個新的 Flutter 頁面 PageA。PageA 中也有一個按鈕,點擊這個按鈕以後會調用 openNativePage 來打開一個新的原生頁面。 整個混合導航棧示例的代碼流程,以下圖所示。經過這張圖,你就能夠把這個示例的整個代碼流程串起來了。
在混合應用工程中,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 容器實例的狀況。