为了把 Flutter 引入到原生工程,我们需要把 Flutter 工程改造为原生工程的一个组件依赖,并以组件化的方式管理不同平台的 Flutter 构建产物,即 Android 平台使用 aar、iOS 平台使用 pod 进行依赖管理。这样,我们就可以在 Android 工程中通过 FlutterView,iOS 工程中通过 FlutterViewController,为 Flutter 搭建应用入口,实现 Flutter 与原生的混合开发方式。
对于混合开发的应用而言,通常我们只会将应用的部分模块修改成 Flutter 开发,其他模块继续保留原生开发,因此应用内除了 Flutter 的页面之外,还会有原生 Android、iOS 的页面。在这种情况下,Flutter 页面有可能会需要跳转到原生页面,而原生页面也可能会需要跳转到 Flutter 页面。这就涉及到了一个新的问题:如何统一管理原生页面和 Flutter 页面跳转交互的混合导航栈。
混合导航栈
混合导航栈,指的是在混合开发中原生页面和Flutter页面相互掺杂,存在于用户视角的页面导航栈视图,如图11-12所示。在混合开发的应用中,原生Android、iOS与Flutter各自实现了一套互不相同的页面映射机制,原生平台采用的是单容器单页面,即一个ViewController或Activity对应一个原生页面;而Flutter采用单容器多页面的机制,即一个ViewController或Activity对应多个Flutter页面。Flutter在原生的导航栈之上又自建了一套Flutter导航栈,这使得原生页面与Flutter页面与之间进行页面切换时,需要处理跨引擎的页面切换问题。
接下来,我们就分别从原生页面跳转至 Flutter 页面,以及从 Flutter 页面跳转至原生页面来看看混合开发的路由管理。
原生页面跳转Flutter页面
从原生页面跳转至 Flutter 页面,实现起来比较简单。因为 Flutter 本身依托于原生提供的容器,即iOS 使用的是FlutterViewController,Android 使用的是Activity 中的 FlutterView。所以我们通过初始化 Flutter 容器,为其设置初始路由页面之后,就可以以原生的方式跳转至 Flutter 页面了。
对于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并没有提供对原生页面的操作方法,所以不能通过直接调用原生平台的方法来实现页面跳转,不过可以使用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跳转原生页面的两种场景的示意图。
使用方法通道实现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两个方法,代码如下。
@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()方法来打开原生页面了,如下所示。
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 Engine 源码,使多 FlutterView 实例对应的多 Flutter Engine 能够在底层共享 Isolate;
- 以闲鱼为代表的共享 FlutterView,即由原生层驱动 Flutter 层渲染内容的方案。
不过,目前这两种解决方案都不够完美。所以,在 Flutter 官方支持多实例单引擎之前,应该尽量使用Flutter去开发一些闭环业务,减少原生页面与Flutter页面之间的交互,尽量避免Flutter页面跳转到原生页面,原生页面又启动一个新的Flutter实例的情况,并且保证应用内不要出现多个 Flutter 容器实例的情况。
原文作者:xiangzhihong
来源:思否