首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

贝壳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 整体架构图

3.2 Android端容器栈管理

在容器栈管理方面,将实现了接口IFlutterViewContainer的FlutterActivity容器缓存到Stack,Flutter层要操作这个容器,需通过uniqueId索引,找到IFlutterViewContainer关联的容器实例。IFlutterViewContainer如下:

代码语言:javascript
复制
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方式不会返回上一页面,而是直接退出:

代码语言:javascript
复制
  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中进行处理:

代码语言:javascript
复制
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端。

代码语言:javascript
复制
/// 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侧滑状态来处理系统侧滑返回手势的状态。

代码语言:javascript
复制
- (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一致,如下所示:

代码语言:javascript
复制
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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/jFaFiKtQQgsVVqHNixQc
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券