贝壳 Flutter 混合容器实践

发布于:2020 年 7 月 3 日 10:04

贝壳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.64 Flutter 官方 -v1.12.13
Flutter 引擎共享 Y Y(不支持动态 initialRoute)
是否支持混合页面之间随意跳转 Y Y(非共享引擎下)
一致的页面生命周期管理 (多 Flutter 页面) Y N
是否支持页面间数据传递 (回传等) Y N
是否支持侧滑手势 N Y(iOS 侧滑,Android 通过返回键)
是否支持跨页的 hero 动画 Y Y
是否提供一致的 Route 方案 Y Y(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 整体架构图

贝壳Flutter混合容器实践

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混合容器实践

如上图中所示,在单容器实例和多引擎多容器实例场景下,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

阅读数:1 发布于:2020 年 7 月 3 日 10:04

评论

发布
暂无评论