Flutter 混合开发框架模式探索
由于 Google 官方提供的 Flutter 混合式开发方案过于简单,仅支持打开一个 Flutter View 的能力,而不支持路由间传参、统一的生命周期、路由栈管理等业务开发中必要的能力,因此我们需要借助第三方混合开发框架(如 Flutter Boost、Thrio、QFlutter 等)的整合能力才能将 Flutter 混合开发模式投入与生产环境。本文中,我们来研究一下这类混合开发框架的职能、架构与源码。
1. 核心职能与框架目标
一个合格的混合开发框架至少需要支持到以下能力:
- 混合路由栈的管理:支持打开任意 Flutter 或 Native 页面。
- 完善的通知机制:如统一的生命周期,路由相关事件通知机制。
对于以上几点目标,我们以 iOS 为例,来逐步挖掘 Flutter 混合开发模式的最佳实现。
注:因为篇幅问题,本文不探究 Android 的实现,从 iOS 切入只是分析问题的一个角度,因 Android 与 iOS 的实现原理一致,具体实现则大同小异。其次因 Channel 通信层代码实现比较繁杂,此文对于 Channel 通信层的讲解仅停留在使用层面上,具体实现读者可以自行研究。
注:本文的 Flutter Boost 版本为 1.12.13,Thrio 的版本为 0.1.0
2. 从 FlutterViewController 开始
在混合开发中,我们使用 Flutter 作为插件化开发,需要起一个 FlutterViewController,这是一个 UIViewController 的实现,其依附于 FlutterEngine,给 Flutter 传递 UIKit 的输入事件,并展示被 FlutterEngine 渲染的每一帧 Flutter views。而这个 FlutterEngine 则充当 Dart VM 和 Flutter 运行时的环境。
需要注意的是,一个 Flutter Engine 只能最多同时运行一个 FlutterViewController。
FlutterEngine: The FlutterEngine class coordinates a single instance of execution for a FlutterDartProject. It may have zero or one FlutterViewController at a time.
启动 Engine:
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
[self.flutterEngine run];
创建 FlutterViewController 并展示
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];
创建一个 FlutterViewController 我们既可以用已经运行中的 FlutterEngine 去初始化,也可以创建的时候同时去隐式启动一个 FlutterEngine(不过不建议这样做,因为按需创建 FlutterEngine 的话,在 FlutterViewController 被 present 出来之后,第一帧图像渲染完之前,将会引入明显的延迟),这里我们用前者的方式去创建。
FlutterEngine: https:// api.flutter.dev/objcdoc /Classes/FlutterEngine.html
FlutterViewController: https:// api.flutter.dev/objcdoc /Classes/FlutterViewController.html
至此,我们通过官方提供的方案在 Native 工程中启动 Flutter Engine 并通过 FlutterViewController 展示 Flutter 页面。
在 Flutter 页面中,我们可以使用 Navigator.push
在打开另一个 Flutter 页面(Route):
因此对于这种路由栈我们很容易实现:
即整个 Flutter 运行在一个单例的 FlutterViewController 容器里,Flutter 内部的所有页面都在这个容器中管理。但如果要实现下面这种 Native 与 Flutter 混合跳转的这种混合路由栈,我们要如何实现呢?
最基本的解决思路是,把这个 FlutterViewController 与 NativeViewController 混合起来,直接让 FlutterViewController 在 iOS 的路由栈中来回移动,如下图所示:
这种方案相对复杂,回到我们上面混合栈的场景,这需要精准记录每个 Flutter 页面和 Native 容器所处的位置,得知道自己 pop 之后应该回到上一层 Flutter 页面,还是切换另一个 NativeViewController,这就得维护好页面索引,并改造原生的pop 时间与 Navigator.pop
事件,使两者统一起来。
我们来看看业界的一些解决方案吧!
3. Flutter Boost
对于混合栈问题,Flutter Boost 会将每个 Flutter 页面用 FlutterViewController 包装起来,使之成为多例,用起来就如同 Webview 一样:
Flutter Boost 的源码之前在另一篇文章中梳理过《Flutter Boost 混合开发实践与源码解析(以 Android 为例)》,那篇文章中梳理了一下 Android 侧打开页面流程的源码,本文中则尽量不重复介绍源码,并且将以 iOS 侧来重点梳理一下 Flutter Boost 究竟是如何实现的。
3.1 从 Native 打开页面
本节分析 Flutter Boost 如何从 Native 打开页面吗,即包含以下两种情况:
- Native -> Flutter
- Native -> Native
在工程中,我们需要接入以下代码集成 Flutter Boost:
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
PlatformRouterImp *router = [PlatformRouterImp new];
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router
onStart:^(FlutterEngine *engine) {
}];
self.window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
[self.window makeKeyAndVisible];
UINavigationController *rvc = [[UINavigationController alloc] initWithRootViewController:tabVC];
router.navigationController = rvc;
self.window.rootViewController = rvc;
return YES;
}
// PlatformRouterImp.m
- (void)openNativeVC:(NSString *)name
urlParams:(NSDictionary *)params
exts:(NSDictionary *)exts{
UIViewController *vc = UIViewControllerDemo.new;
BOOL animated = [exts[@"animated"] boolValue];
if([params[@"present"] boolValue]){
[self.navigationController presentViewController:vc animated:animated completion:^{
}];
}else{
[self.navigationController pushViewController:vc animated:animated];
}
}
- (void)open:(NSString *)name
urlParams:(NSDictionary *)params
exts:(NSDictionary *)exts
completion:(void (^)(BOOL))completion
{
if ([name isEqualToString:@"native"]) { // 模拟打开native页面
[self openNativeVC:name urlParams:params exts:exts];
return;
}
BOOL animated = [exts[@"animated"] boolValue];
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController pushViewController:vc animated:animated];
if(completion) completion(YES);
}
可以发现,我们在工程中首先要启动引擎,对应的 Flutter Boost 源码如下:
// FlutterBoostPlugin.m
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
engine:(FlutterEngine *)engine
pluginRegisterred:(BOOL)registerPlugin
onStart:(void (^)(FlutterEngine * _Nonnull))callback{
static dispatch_once_t onceToken;
__weak __typeof__(self) weakSelf = self;
dispatch_once(&onceToken, ^{
__strong __typeof__(weakSelf) self = weakSelf;
FLBFactory *factory = FLBFactory.new;
self.application = [factory createApplication:platform];
[self.application startFlutterWithPlatform:platform
withEngine:engine
withPluginRegisterred:registerPlugin
onStart:callback];
});
}
// FLBFlutterApplication.m
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
withEngine:(FlutterEngine* _Nullable)engine
withPluginRegisterred:(BOOL)registerPlugin
onStart:(void (^)(FlutterEngine *engine))callback
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.platform = platform;
self.viewProvider = [[FLBFlutterEngine alloc] initWithPlatform:platform engine:engine];
self.isRunning = YES;
if(registerPlugin){
Class clazz = NSClassFromString(@"GeneratedPluginRegistrant");
FlutterEngine *myengine = [self.viewProvider engine];
if (clazz && myengine) {
if ([clazz respondsToSelector:NSSelectorFromString(@"registerWithRegistry:")]) {
[clazz performSelector:NSSelectorFromString(@"registerWithRegistry:")
withObject:myengine];
}
}
}
if(callback) callback(self.viewProvider.engine);
});
}
可以发现启动引擎时 startFlutterWithPlatform 需要传入路由管理类,而在 FLBPlatform.h 中可以看到 open 接口是由业务侧 Native 传过来的实现。
// PlatformRouterImp.m
- (void)open:(NSString *)name
urlParams:(NSDictionary *)params
exts:(NSDictionary *)exts
completion:(void (^)(BOOL))completion
{
if ([name isEqualToString:@"native"]) { // 模拟打开native页面
[self openNativeVC:name urlParams:params exts:exts];
return;
}
BOOL animated = [exts[@"animated"] boolValue];
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.naviga