通常纯Flutter应用的页面路由直接由Flutter自身来管理,但是对于原生App要引入Flutter技术,就会涉及到原生页面与Flutter页面之间切换,此时的页面路由需要单独管理和实现。
本文将从方案选取、容器栈管理、Flutter导航栈管理以及实践中遇到的问题等方面来介绍贝壳Flutter容器的实现。
页面:通常说的页面在Flutter中指的是Route,在Android中指的是Activity,iOS中指的是ViewController。
Flutter容器:Flutter容器提供了Dart代码的运行时环境,一般包含容器类、显示视图和引擎三部分:
initialRoute:initialRoute是Flutter页面的路由标识。
Navigator:Navigator是管理Route的类,它通过Overlay栈来管理活动的Route。
我们在方案调研时一方面比较了官方与闲鱼的实现,另一方面在路由管理这一关键技术上做了调研以及Demo验证。
对比项 | 闲鱼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) |
版本升级适配成本 | 高 | 低 |
经过探讨,主要有三种实现方式:
a. 修改底层Engine代码使其动态传递生效
结论:Engine的修改涉及双端Native的修改,稳定性未知,还需考虑本地Engine的替换成本,从Flutter GitHub上看官方会支持动态传递能力,所以这个方式暂不考虑。
b. MethodChannel提前将Flutter的initialRoute传入
结论:需要新增加一个MethodChannel,优势不明显。
c. 在容器生命周期中将initialRoute动态交由自定义的NavigatorManager来展示
结论:闲鱼FlutterBoost采用的就是这种实现,其在稳定性上有保证。
经过上述对比与调研,可以发现:
官方容器方案:只需少量定制即可用,但共享Engine时无法动态替换initialRoute;
闲鱼flutterboost方案:功能丰富,可共享Engine, 但版本升级时接口差异较大,版本适配成本较高。
最终我们决定:在整体方案上采用官方1.12的共享引擎的方式,在路由管理的实现上借鉴闲鱼FlutterBoost的实现。
在容器栈管理方面,将实现了接口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");
}
}
在iOS中侧滑返回是一个基础功能,但存在侧滑返回失效的问题,有两种情况:
a. 开启系统侧滑返回、同时Flutter容器内打开多个页面, 导致Flutter页面无法通过侧滑逐级返回。
b. 关闭系统侧滑返回、同时Flutter容器内只有一个页面,导致Flutter页面无法通过侧滑退出。
针对该问题,我们做了如下修改:
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);
}
}
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
如上图中所示,在单容器实例和多引擎多容器实例场景下,Flutter官方的做法是每个容器对应一个Navigator。在实际业务开发中主要是多引擎多容器的场景,但是多引擎多容器会带来引擎内存开销,因此需要单引擎多容器方案来替代。
单引擎多容器实现方案是:
目前容器支持两种参数格式:
协议包含三部分:
为兼容适配贝壳Native Router sdk的跳转场景,在进行参数解析时,Flutter内部会对参数的编码做校验,并将中文字符做Encode处理。
官方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跳转
}
}
目前该混合容器方案已在贝壳APP、置业顾问APP上线,在性能与稳定性上表现良好。案场APP也正在接入中。如果其他业务有需求,可以联系我们。
我们这套容器方案目前可以满足绝大部分业务使用场景,针对Flutter页面build()导致的重复网络请求、iOS侧滑失效等问题,我们都做了针对性修改。
针对Android端广播、iOS端通知中心等功能,如有业务需求,我们也将在后续迭代中给予支持。
领取专属 10元无门槛券
私享最新 技术干货