贝壳Flutter混合容器实践

2020 年 7 月 03 日

贝壳Flutter混合容器实践

通常纯 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 官方与闲鱼对比


对比项闲鱼FlutterBoost-v0.1.64Flutter官方-v1.12.13
Flutter引擎共享YY(不支持动态initialRoute)
是否支持混合页面之间随意跳转YY(非共享引擎下)
一致的页面生命周期管理(多Flutter页面)YN
是否支持页面间数据传递(回传等)YN
是否支持侧滑手势NY(iOS侧滑,Android通过返回键)
是否支持跨页的hero动画YY
是否提供一致的Route方案YY(initialRoute)
版本升级适配成本


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 如下:


public interface IFlutterViewContainer {    String uniqueId();
String initialRoute(); // 当前Flutter页面所在的Activity Activity getContainerActivity(); // 提供数据 void finishContainer(Map<String,Object> result); // 接收数据 void onContainerResult(int requestCode, int resultCode, Intent data);
}
复制代码


这里遇到一个问题:默认情况下返回键响应的是 NavigationChannel popRoute;多容器实例情况下,popRoute 方式不会返回上一页面,而是直接退出:


  void onBackPressed() {    ensureAlive();    if (flutterEngine != null) {      Log.v(TAG, "Forwarding onBackPressed() to FlutterEngine.");      flutterEngine.getNavigationChannel().popRoute();    } else {      Log.w(TAG, "Invoked onBackPressed() before FlutterFragment was attached to an Activity.");    }  }
复制代码


为解决这个问题,我们对返回事件进行了拦截并在 RunnerManager 中进行处理:


public class RunnerFlutterActivity extends FlutterActivity implements IFlutterViewContainer {    // ...    @Override    public void onBackPressed() {        //super.onBackPressed();        invokeChannelWithParams("onBackPressed");    }}
复制代码


3.3 iOS 端侧滑返回的支持


在 iOS 中侧滑返回是一个基础功能,但存在侧滑返回失效的问题,有两种情况:


a. 开启系统侧滑返回、同时 Flutter 容器内打开多个页面, 导致 Flutter 页面无法通过侧滑逐级返回。


b. 关闭系统侧滑返回、同时 Flutter 容器内只有一个页面,导致 Flutter 页面无法通过侧滑退出。


针对该问题,我们做了如下修改:


1)Dart 端修改


Dart 端修改主要是来监控 Navigator 栈变化,将侧滑返回的状态传递给 iOS 端。


/// IOS手势返回class IosGestureObserver extends NavigatorObserver{
@override void didPush(Route route, Route previousRoute) { if (route.navigator.canPop()) { //通知Native Disable navigation Gesture } super.didPush(route, previousRoute); } @override void didPop(Route route, Route previousRoute) { if (!previousRoute.navigator.canPop()) { //通知Native Enable navigation Gesture } super.didPop(route, previousRoute); }
}
复制代码


2)iOS 端修改


iOS 端收到消息后会将 Flutter 侧滑状态保留起来,传递给代理,代理可以根据 Flutter 侧滑返回状态来做自定义处理。如果代理没有实现,则判断进入容器时 iOS 系统侧滑返回手势的状态;如果为 Yes,则根据 Flutter 侧滑状态来处理系统侧滑返回手势的状态。


- (void)notificationEnableUserGesture:(NSNotification *)notification {  NSDictionary *userInfo = notification.userInfo;  NSNumber *enableNumber = userInfo[@"enableUserGesture"];  self.flutterCanPop = [enableNumber boolValue];  if ([self.delegate respondsToSelector:@selector(flutterPagePopStateChange:)]) {    [self.delegate flutterViewController:self PagePopStateChange:self.flutterCanPop];   } else {     if (self.originCanPop) {       self.navigationController.interactivePopGestureRecognizer.enabled = !self.flutterCanPop;     }   }  return; }
@protocol BKFlutterViewControllerDelegate <NSObject>- (void)flutterViewController:(BKFlutterViewController *)viewController PagePopStateChange:(BOOL)canPop; @end
复制代码


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&params={"tel": 10086, "city":"bj"}
    


协议包含三部分:


  • 路由头:

  • 找到对应的Flutter容器,如lianjialink://flutter/page
    
  • 基础参数(key固定):

  • flutter_url=my/flutter/page(Flutter页面标识)
    params={业务json参数}
    
  • 业务参数:

  • url Query形式: 如 &tel=10086&city=bj
        Json形式: 如 &params={"tel": 10086, "city":"bj"}
    


为兼容适配贝壳 Native Router sdk 的跳转场景,在进行参数解析时,Flutter 内部会对参数的编码做校验,并将中文字符做 Encode 处理。


3.6 Android Flutter Fragment


官方 FlutterFragemt 与 FlutterActivity 类似,都无法在共享 Engine 时动态设定 Flutter 页面路由。


我们将 Activity 共享 Engine 的逻辑移植到 Fragment 中,使其充当容器的角色。目前暴露给业务方的使用方式与 Activity 一致,如下所示:


IOperationCallback router = new IOperationCallback() {    @Override    public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode) {        // Flutter activity内startActivity/ForResult跳转    }
@Override public void openContainerFromFragment(Fragment fragment, String url, Map<String, Object> urlParams, int requestCode) { // Flutter fragment内startActivity/ForResult跳转 }}
复制代码


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)。


原文链接


https://mp.weixin.qq.com/s?__biz=MzIyMTg0OTExOQ==&mid=2247485738&idx=2&sn=f23fe71d3466c2117708b1e2c79625d0&chksm=e8373a5adf40b34cef5f395c8f7c792f067b8d41ef805798c800bbc266390922961ff7c10682&scene=27#wechat_redirect


2020 年 7 月 03 日 10:041736

评论

发布
暂无评论
发现更多内容

Docker网络学习第一篇:Linux虚拟网络

Lazy

Docker Linux 网络

秒懂云通信:通信圈黑话大盘点

巨侠说

云通信 通信云

Malagu 框架的认证与授权【借鉴 Spring Security 和 aws iam 的设计】

木香丘

身份认证 权限系统

SQL Server 报表服务

JackWangGeek

SharePoint

用Report Builder 创建报表

JackWangGeek

SharePoint

Java8——方法引用

Java旅途

java8 方法引用

开发框架文档体系化的思考

vivo互联网技术

框架开发

【进收藏夹吃灰系列】——Java基础快速扫盲

Noneplus

Java

实战技巧,Vue原来还可以这样写

前端有的玩

Java Vue 前端 技巧

【面试题系列】——Java基础

Noneplus

Java

30岁+程序员职场攻略:找到自己的“职业锚”乘风破浪

华为云开发者社区

程序员 AI 开发者 职场 程序员成长

6种快速统计代码执行时间的方法,真香!

王磊

Java

CAP原理

李白

“Python的单例模式有四种写法,你知道么?”——孔乙己

Young先生

Python 设计模式 单例模式

从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局BuildContext问题

渔子长

flutter 前端 跨平台 React

猿灯塔:spring Boot Starter开发及源码刨析(五)

猿灯塔

spring 猿灯塔

Doris 临时失效处理过程

石印掌纹

第六周总结

石印掌纹

配置 SharePoint Server for Reporting Services

JackWangGeek

SharePoint

Malagu 框架开发 React 应用新体验

木香丘

Serverless React 微前端 微应用 Malagu

那些年,我在阿里当数据开发

DeeperMan

大数据

关于如何判断一个list是否为空的思考

Leetao

Python Python基础知识 列表

纯CSS实现自定义单选框和复选框

爱嘤嘤嘤斯坦

CSS Java 编程语言 标签

Web经典B/S快速开发框架,强大后台+简洁UI一体化开发工具

力软.net/java开发平台

C# .net 软件开发 web开发

5万字、97 张图总结操作系统核心知识点

cxuan

操作系统 计算机

昨天、今天、明天

escray

一张PDF了解JDK10 GC调优秘籍-附PDF下载

程序那些事

Java jdk JVM GC JDK10

架构师训练营作业 -- Week 6

吴炳华

极客大学架构师训练营

Docker网络学习第二篇-认识iptables

Lazy

Docker Linux 网络

文档写作利器:Markdown

xcbeyond

markdown

MySQL性能优化(一):MySQL架构与核心问题

xcbeyond

MySQL MySQL性能优化

贝壳Flutter混合容器实践-InfoQ