通常纯 Flutter 应用的页面路由直接由 Flutter 自身来管理,但是对于原生 App 要引入 Flutter 技术,就会涉及到原生页面与 Flutter 页面之间切换,此时的页面路由需要单独管理和实现。
本文将从方案选取、容器栈管理、Flutter 导航栈管理以及实践中遇到的问题等方面来介绍贝壳 Flutter 容器的实现。
1. Flutter 的相关概念
页面:通常说的页面在 Flutter 中指的是 Route,在 Android 中指的是 Activity,iOS 中指的是 ViewController。
Flutter 容器:Flutter 容器提供了 Dart 代码的运行时环境,一般包含容器类、显示视图和引擎三部分:
FlutterActivity/FlutterViewController:官方提供的显示Flutter⻚面容器基础类,FlutterActivity是Android端基础类,FlutterViewController对应iOS端基础类。
FlutterView:是显示Flutter Widget的视图。
FlutterEngine:是一个用于承载Flutter应用的可移植的运行时。它实现了Flutter的核心库,包括动画和图形、文件和网络I/O、可访问性支持、插件架构,以及Dart运行时和编译工具链。
initialRoute:initialRoute 是 Flutter 页面的路由标识。
Navigator:Navigator 是管理 Route 的类,它通过 Overlay 栈来管理活动的 Route。
2. 方案调研
我们在方案调研时一方面比较了官方与闲鱼的实现,另一方面在路由管理这一关键技术上做了调研以及 Demo 验证。
2.1 官方与闲鱼对比
2.2 路由管理调研
经过探讨,主要有三种实现方式:
a. 修改底层 Engine 代码使其动态传递生效
结论:Engine 的修改涉及双端 Native 的修改,稳定性未知,还需考虑本地 Engine 的替换成本,从 Flutter GitHub 上看官方会支持动态传递能力,所以这个方式暂不考虑。
b. MethodChannel 提前将 Flutter 的 initialRoute 传入
结论:需要新增加一个 MethodChannel,优势不明显。
c. 在容器生命周期中将 initialRoute 动态交由自定义的 NavigatorManager 来展示
结论:闲鱼 FlutterBoost 采用的就是这种实现,其在稳定性上有保证。
2.3 方案确定
经过上述对比与调研,可以发现:
官方容器方案:只需少量定制即可用,但共享 Engine 时无法动态替换 initialRoute;
闲鱼 flutterboost 方案:功能丰富,可共享 Engine, 但版本升级时接口差异较大,版本适配成本较高。
最终我们决定:在整体方案上采用官方 1.12 的共享引擎的方式,在路由管理的实现上借鉴闲鱼 FlutterBoost 的实现。
3. 容器方案的实现
3.1 整体架构图
3.2 Android 端容器栈管理
在容器栈管理方面,将实现了接口 IFlutterViewContainer 的 FlutterActivity 容器缓存到 Stack,Flutter 层要操作这个容器,需通过 uniqueId 索引,找到 IFlutterViewContainer 关联的容器实例。IFlutterViewContainer 如下:
这里遇到一个问题:默认情况下返回键响应的是 NavigationChannel popRoute;多容器实例情况下,popRoute 方式不会返回上一页面,而是直接退出:
为解决这个问题,我们对返回事件进行了拦截并在 RunnerManager 中进行处理:
3.3 iOS 端侧滑返回的支持
在 iOS 中侧滑返回是一个基础功能,但存在侧滑返回失效的问题,有两种情况:
a. 开启系统侧滑返回、同时 Flutter 容器内打开多个页面, 导致 Flutter 页面无法通过侧滑逐级返回。
b. 关闭系统侧滑返回、同时 Flutter 容器内只有一个页面,导致 Flutter 页面无法通过侧滑退出。
针对该问题,我们做了如下修改:
1)Dart 端修改
Dart 端修改主要是来监控 Navigator 栈变化,将侧滑返回的状态传递给 iOS 端。
2)iOS 端修改
iOS 端收到消息后会将 Flutter 侧滑状态保留起来,传递给代理,代理可以根据 Flutter 侧滑返回状态来做自定义处理。如果代理没有实现,则判断进入容器时 iOS 系统侧滑返回手势的状态;如果为 Yes,则根据 Flutter 侧滑状态来处理系统侧滑返回手势的状态。
3.4 多容器实例 Navigator 栈管理
如上图中所示,在单容器实例和多引擎多容器实例场景下,Flutter 官方的做法是每个容器对应一个 Navigator。在实际业务开发中主要是多引擎多容器的场景,但是多引擎多容器会带来引擎内存开销,因此需要单引擎多容器方案来替代。
单引擎多容器实现方案是:
在打开Flutter页面后,由RunnerManager拦截默认Navigator,将容器uniqueId属性以及Flutter路由表复制到自定义RunnerNavigator,将RunnerNavigator以OverlayEntry的形式记录在栈中。
然后Native容器与Flutter导航路由通过uniqueId绑定,这样在Flutter页面的操作可以经过uniqueId在Native端容器栈响应。
当重新打开一个容器实例,RunnerManager会重置该Navigator的initialRoute,然后交由Navigator打开对应的Flutter页面。
3.5 Flutter 页面路由协议
目前容器支持两种参数格式:
格式1:
lianjialink://flutter/page?flutter_url=my/flutter/page&tel=10086&city=bj
格式2:
lianjialink://flutter/page?flutter_url=my/flutter/page¶ms={"tel": 10086, "city":"bj"}
协议包含三部分:
路由头:
找到对应的Flutter容器,如lianjialink://flutter/page
基础参数(key固定):
flutter_url=my/flutter/page(Flutter页面标识) params={业务json参数}
业务参数:
url Query形式: 如 &tel=10086&city=bj Json形式: 如 ¶ms={"tel": 10086, "city":"bj"}
为兼容适配贝壳 Native Router sdk 的跳转场景,在进行参数解析时,Flutter 内部会对参数的编码做校验,并将中文字符做 Encode 处理。
3.6 Android Flutter Fragment
官方 FlutterFragemt 与 FlutterActivity 类似,都无法在共享 Engine 时动态设定 Flutter 页面路由。
我们将 Activity 共享 Engine 的逻辑移植到 Fragment 中,使其充当容器的角色。目前暴露给业务方的使用方式与 Activity 一致,如下所示:
4. 总结
目前该混合容器方案已在贝壳 APP、置业顾问 APP 上线,在性能与稳定性上表现良好。案场 APP 也正在接入中。如果其他业务有需求,可以联系我们。
我们这套容器方案目前可以满足绝大部分业务使用场景,针对 Flutter 页面 build()导致的重复网络请求、iOS 侧滑失效等问题,我们都做了针对性修改。
针对 Android 端广播、iOS 端通知中心等功能,如有业务需求,我们也将在后续迭代中给予支持。
5. 参考资料
https://flutter.dev/docs/development/add-to-app
https://github.com/alibaba/flutter_boost
本文转载自公众号贝壳产品技术(ID:beikeTC)。
原文链接:
评论