如何实现 Flutter 与原生的混合开发方式?详解Flutter混合开发的路由栈管理(深度好文)

为了把 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页面与之间进行页面切换时,需要处理跨引擎的页面切换问题。

image

接下来,我们就分别从原生页面跳转至 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);

运行项目代码,最终的效果下图所示。

image

对于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跳转原生页面的两种场景的示意图。

image

使用方法通道实现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 来打开一个新的原生页面。
整个混合导航栈示例的代码流程,如下图所示。通过这张图,你就可以把这个示例的整个代码流程串起来了。

image

在混合应用工程中,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

原文链接:人类身份验证 - SegmentFault

来源:思否

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容