当前项目是flutter和iOS、安卓原生客户端混合使用的,有flutter和原生页面互相调用的需求,所以之前使用了hybrid_stack_manager来管理混合栈。

//Push
UINavigationController *currentNavigation = (UINavigationController*)[UIApplication sharedApplication].delegate.window.rootViewController;
FlutterViewWrapperController *viewController = [[FlutterViewWrapperController alloc] initWithURL:[NSURL URLWithString:aUrl] query:query nativeParams:params];
viewController.viewWillAppearBlock = ^(){
//Process first & later message sending according distinguishly.
if(sIsFirstPush){
[HybridStackManager sharedInstance].mainEntryParams = arguments;
sIsFirstPush = FALSE;
}
else{
[methodChann invokeMethod:@"openURLFromFlutter" arguments:arguments result:^(id _Nullable result) {
}];
}
};
[currentNavigation pushViewController:viewController animated:YES];//直接使用navigationController
在flutter端
pushPageWithOptionsFromFlutter({RouterOption routeOption, bool animated}) {
Widget page =
Router.sharedInstance().pageFromOption(routeOption: routeOption);
if (page != null) {
XMaterialPageRoute pageRoute = new XMaterialPageRoute(
settings: new RouteSettings(name: routeOption.userInfo),
animated: animated,
builder: (BuildContext context) {
return page;
});
Navigator.of(globalKeyForRouter.currentContext).push(pageRoute); //直接使用Navigator
HybridStackManagerPlugin.hybridStackManagerPlugin
.updateCurFlutterRoute(routeOption.userInfo);
} else {
HybridStackManagerPlugin.hybridStackManagerPlugin.openUrlFromNative(
url: routeOption.url,
query: routeOption.query,
params: routeOption.params);
}
NavigatorState navState = Navigator.of(globalKeyForRouter.currentContext);
List<Route<dynamic>> navHistory = navState.history;
}
FlutterEngine消耗的资源很大,而且启用多个flutterEngine会造成不同flutter页面通信困难,所以在hybrid_stack_manager中不同的flutterWrapperViewController中也是共用同一个FlutterViewController和flutterEngine的。这样基本解决了flutter和原生页面互相打开的问题。
但是我们很快就发现要打开flutter页面必须通过push的方法,存在很大的局限,在app使用tabbarViewController的时候,如果有一个tab中的页面是flutter页面,不能通过navigationController push的方式加载。 screenshot.png


Flutter Boost同样是闲鱼团队出品的混合栈框架,迁移到这个框架后,解决了上面提到的所有问题。 screenshot.png

FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController pushViewController:vc animated:animated];
从上面代码可以看到,FLBFlutterViewContainer是flutter页面的原生容器,每一个flutter页面都对应一个FLBFlutterViewContainer,通过调用setName:params方法到flutter端找到对应的widget,加载到这个原生容器中。 最关键的是[self.navigationController pushViewController:vc animated:animated];这句代码我们业务代码自己调用的,并不像hybrid_stack_manager中强制有框架调用navigationController 的push方法。换句话说,flutter boost只完成从flutter widget到原生页面的转化,具体怎么用是留给业务代码控制的。 在flutter端也没有直接使用navigator push这样简单粗暴的方式,而是自己维护了一个导航栈。
BoostContainer _onstage;//当前显示的页面
final List<BoostContainer> _offstage = <BoostContainer>[];//之前的页面,用做缓存


MMCommonNAVI *rootNav = [[MMCommonNAVI alloc]initWithRootViewController: [ZNUIManager sharedInstance].rootTab];
//flutter boost启动
ZNFlutterRouter *flutterRouter = [ZNFlutterRouter sharedRouter];
flutterRouter.navigationController = rootNav;
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:flutterRouter
onStart:^(FlutterViewController *fvc) {
}];
[rootNav setNavigationBarHidden:YES];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = rootNav;
[self.window makeKeyAndVisible];
需要实现一个自己的路由控制类,并且传入navigationController。
@interface ZNFlutterRouter : NSObject<FLBPlatform>
@property (nonatomic,strong) UINavigationController *navigationController;
+ (ZNFlutterRouter *)sharedRouter;
@end
- (void)openPage:(NSString *)name
params:(NSDictionary *)params
animated:(BOOL)animated
completion:(void (^)(BOOL))completion
{
//打开原生页面,从原生或者flutter页面调用最终都会到这里
if([name isEqualToString:@"hrd://product_detail"]){
// 跳转至设备详情
ZNProductDetailViewController *next = [ZNProductDetailViewController new];
next.sn = [params objectForKey: @"pid"];
[[[ZNUIManager sharedInstance] appRootNavi] pushViewController:next animated:YES];
}
//打开flutter页面,所有的flutter页面都会走到下面,设置不同的name就会生成widget对应的原生页面
else if([params[@"present"] boolValue]){
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController presentViewController:vc animated:animated completion:^{}];
}else{
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController pushViewController:vc animated:animated];
}
}
打开flutter和原生页面最终都会走到openPage这个方法中,从原生打开页面是原生直接调用openPage方法,而从flutter侧打开页面是通过methodChannel调用openPage方法。
在flutter侧也需要注册pageName和widget的对应关系。
static void registerPageBuilder(){
//注册flutter页面
FlutterBoost.singleton.registerPageBuilders({
'hrd://product_list':(pageName, params, uniqueId) {
RouterOption option = new RouterOption(url: pageName,params: params);
return new ProductList(option);
},
//tabbar第三个tab,push进来页面和直接展示在tab中的页面采用同样的配置方式
'hrd://board_main':(pageName, params, uniqueId) {
return new BoardMain();
},
});
FlutterBoost.handleOnStartPage();
}
上面代码可以看到hrd://product_list 这是对应一个push进来的页面,而hrd://board_main是app启动后就初始化在tabbarViewController中的,两个页面都是相同的初始化和注册方式,具体怎么展示由原生端决定。
原生侧打开flutter页面调用方法如下,要传入pageName和params。
[ZNFlutterRouter.sharedRouter openPage:@"hrd://upload_contract" params:@{@"orderCode":self.orderCode,@"customerCode":self.orderDetailModel.customerCode,@"changeCode":@"",@"state":@"0"} animated:YES completion:^(BOOL finished) {
}];
flutter打开原生页面调用方法如下:
FlutterBoost.singleton.openPage(url, params, animated: animated,resultHandler: resultHandler)
需要注意的是,从页面效果上看原生打开flutter是push过来的,但是由于是直接把widget放入原生的viewController里面的,所以第一个flutter页面的左上角没有回退按钮的,我们需要自己在Appbar中加入回退按钮并调用回退的方法。
return Scaffold(
appBar: AppBar(
title: Text(‘Flutter页面'),
elevation: 0.0,
leading: IconButton(icon: Icon(Icons.arrow_back_ios), onPressed: popCurrentPage),
),
body: Container(),
);
flutter关闭当前页面的调用方法如下:
FlutterBoost.singleton.closeCurPage(params);
集成flutter_boost的过程是很顺利的,不过原理理解废了一番功夫,主要是由于源代码里面注释太少了,而代码量又特别大,我画了个类图帮助理解,可以从这个类图看到框架中各个类的关系。 Container.jpg
