Flutter Boost 混合开发框架
目录
- 一、Flutter Boost简介
- 二、Flutter Boost集成
- 2.1 Android集成
- 2.2 iOS集成
- 三、Flutter Boost架构
- 四、FlutterBoost3.0更新
- 4.1 不入侵引擎
- 4.2 不区分Androidx和Support分支
- 4.3 双端设计统一,接口统一
- 4.4 支持 【打开flutter页面不再打开容器】 场景
- 4.5 生命周期的精准通知
- 4.6 其他Issue
一、Flutter Boost简介
众所周知,Flutter是一个由C++实现的Flutter Engine和由Dart实现的Framework组成的跨平台技术框架。其中,Flutter Engine负责线程管理、Dart VM状态管理以及Dart代码加载等工作,而Dart代码所实现的Framework则负责上层业务开发,如Flutter提供的组件等概念就是Framework的范畴。
随着Flutter的发展,国内越来越多的App开始接入Flutter。为了降低风险,大部分App采用渐进式方式引入Flutter,在App里选几个页面用Flutter来编写,但都碰到了相同的问题,在原生页面和Flutter页面共存的情况下,如何管理路由,以及原生页面与Flutter页面之间的切换和通信都是混合开发中需要解决的问题。然而,官方没有提供明确的解决方案,只是在混合开发时,官方建议开发者,应该使用同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎里面的资源。换句话说,官方希望所有的绘制窗口共享同一个主Isolate,而不是出现多个主Isolate的情况。不过,对于现在已经出现的多引擎模式问题,Flutter官方也没有提供好的解决方案。除了内存消耗严重外,多引擎模式还会带来如下一些问题。
- 冗余资源问题。多引擎模式下每个引擎的Isolate是相互独立的,虽然在逻辑上这并没有什么坏处,但是每个引擎底层都维护了一套图片缓存等比较消耗内存的对象,因此设备的内存消耗是非常严重的。
- 插件注册问题。在Flutter插件中,消息传递需要依赖Messenger,而Messenger是由FlutterViewController去实现的。如果一个应用中同时存在多个FlutterViewController,那么插件的注册和通信将会变得混乱且难以维护。
- Flutter组件和原生页面的差异化问题。通常,Flutter页面是由组件构成的,原生页面则是由ViewController或者Activity构成的。逻辑上来说,我们希望消除Flutter页面与原生页面的差异,否则在进行页面埋点和其它一些操作时增加一些额外的工作量。
- 增加页面通信的复杂度。如果所有的Dart代码都运行在同一个引擎实例中,那么它们会共享同一个Isolate,可以用统一的框架完成组件之间的通信,但是如果存在多个引擎实例会让Isolate的管理变得更加复杂。
如果不解决多引擎问题,那么混合项目的导航栈如下图所示。
目前,对于原生工程混编Flutter工程出现的多引擎模式问题,国内主要有两种解决方案,一种是字节跳动的修改Flutter Engine源码方案,另一种是闲鱼开源的FlutterBoost。由于字节跳动的混合开发的方案没有开源,所以现在能使用的就剩下FlutterBoost方案。
FlutterBoost是闲鱼技术团队开发的一个可复用页面的插件,旨在把Flutter容器做成类似于浏览器的加载方案。为此,闲鱼技术团队为希望FlutterBoost能完成如下的基本功能:
- 可复用的通用型混合开发方案。
- 支持更加复杂的混合模式,比如支持Tab切换的场景。
- 无侵入性方案,使用时不再依赖修改Flutter的方案。
- 支持对页面生命周期进行统一的管理。
- 具有统一明确的设计概念。
并且,最近Flutter Boost升级了3.0版本,并带来了如下的一些更新:
- 不侵入引擎,兼容Flutter的各种版本,Flutter sdk的升级不需要再升级FlutterBoost,极大降低升级成本。
- 不区分Androidx和Support分支。
- 简化架构和接口,和FlutterBoost2.0比,代码减少了一半。
- 双端统一,包括接口和设计上的统一。
- 支持打开Flutter页面,不再打开容器场景。
- 页面生命周期变化通知更方便业务使用。
- 解决了2.0中的遗留问题,例如,Fragment接入困难、页面关闭后不能传递数据、dispose不执行,内存占用过高等。
二、Flutter Boost集成
在原生项目中集成Flutter Boost只需要将Flutter Boost看成是一个插件工程即可。和其他Flutter插件的集成方式一样,使用FlutterBoost之前需要先添加依赖。使用Android Studio打开混合工程的Flutter工程,在pubspec.yaml中添加FlutterBoost依赖插件,如下所示。
flutter_boost: git: url: 'https://github.com/alibaba/flutter_boost.git' ref: 'v3.0-hotfixes'
需要说明的是,此处的所依赖的FlutterBoost的版本与Flutter的版本是对应的,如果不对应使用过程中会出现版本不匹配的错误。然后,使用flutter packages get命令将FlutterBoost插件拉取到本地。
2.1 Android集成
使用Android Studio打开新建的原生Android工程,在原生Android工程的settings.gradle文件中添加如下代码。
setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'flutter_library/.android/include_flutter.groovy'))
然后,打开原生Android工程app目录下的build.gradle文件,继续添加如下依赖脚本。
dependencies { implementation project(':flutter_boost') implementation project(':flutter') }
重新编译构建原生Android工程,如果没有任何错误则说明Android成功了集成FlutterBoost。使用Flutter Boost 之前,需要先执行初始化。打开原生Android工程,新建一个继承FlutterApplication的Application,然后在onCreate()方法中初始化FlutterBoost,代码如下。
public class MyApplication extends FlutterApplication { @Override public void onCreate() { super.onCreate(); FlutterBoost.instance().setup(this, new FlutterBoostDelegate() { @Override public void pushNativeRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new Intent(FlutterBoost.instance().currentActivity(), NativePageActivity.class); FlutterBoost.instance().currentActivity().startActivity(intent); } @Override public void pushFlutterRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new FlutterBoostActivity.CachedEngineIntentBuilder(FlutterBoostActivity.class, FlutterBoost.ENGINE_ID) .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque) .destroyEngineWithActivity(false) .url(pageName) .urlParams(arguments) .build(FlutterBoost.instance().currentActivity()); FlutterBoost.instance().currentActivity().startActivity(intent); } },engine->{ engine.getPlugins(); } ); } }
然后,打开原生Android工程下的AndroidManifest.xml文件,将Application替换成自定义的MyApplication,如下所示。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.idlefish.flutterboost.example"> <application android:name="com.idlefish.flutterboost.example.MyApplication" android:label="flutter_boost_example" android:icon="@mipmap/ic_launcher"> <activity android:name="com.idlefish.flutterboost.containers.FlutterBoostActivity" android:theme="@style/Theme.AppCompat" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" > <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/> </activity> <meta-data android:name="flutterEmbedding" android:value="2"> </meta-data> </application> </manifest>
由于Flutter Boost 是以插件的方式集成到原生Android项目的,所以我们可以在Native 打开和关闭Flutter模块的页面。
FlutterBoost.instance().open("flutterPage",params); FlutterBoost.instance().close("uniqueId");
而Flutter Dart的使用如下。首先,我们可以在main.dart文件的程序入口main()方法中进行初始化。
void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { static Map<String, FlutterBoostRouteFactory> routerMap = { '/': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => Container()); }, 'embedded': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => EmbeddedFirstRouteWidget()); }, 'presentFlutterPage': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => FlutterRouteWidget( params: settings.arguments, uniqueId: uniqueId, )); }}; Route<dynamic> routeFactory(RouteSettings settings, String uniqueId) { FlutterBoostRouteFactory func =routerMap[settings.name]; if (func == null) { return null; } return func(settings, uniqueId); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return FlutterBoostApp( routeFactory ); }
当然,还可以监听页面的生命周期,如下所示。
class SimpleWidget extends StatefulWidget { final Map params; final String messages; final String uniqueId; const SimpleWidget(this.uniqueId, this.params, this.messages); @override _SimpleWidgetState createState() => _SimpleWidgetState(); } class _SimpleWidgetState extends State<SimpleWidget> with PageVisibilityObserver { static const String _kTag = 'xlog'; @override void didChangeDependencies() { super.didChangeDependencies(); print('$_kTag#didChangeDependencies, ${widget.uniqueId}, $this'); } @override void initState() { super.initState(); PageVisibilityBinding.instance.addObserver(this, ModalRoute.of(context)); print('$_kTag#initState, ${widget.uniqueId}, $this'); } @override void dispose() { PageVisibilityBinding.instance.removeObserver(this); print('$_kTag#dispose, ${widget.uniqueId}, $this'); super.dispose(); } @override void onForeground() { print('$_kTag#onForeground, ${widget.uniqueId}, $this'); } @override void onBackground() { print('$_kTag#onBackground, ${widget.uniqueId}, $this'); } @override void onAppear(ChangeReason reason) { print('$_kTag#onAppear, ${widget.uniqueId}, $reason, $this'); } void onDisappear(ChangeReason reason) { print('$_kTag#onDisappear, ${widget.uniqueId}, $reason, $this'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('tab_example'), ), body: SingleChildScrollView( physics: BouncingScrollPhysics(), child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: const EdgeInsets.only(top: 80.0), child: Text( widget.messages, style: TextStyle(fontSize: 28.0, color: Colors.blue), ), alignment: AlignmentDirectional.center, ), Container( margin: const EdgeInsets.only(top: 32.0), child: Text( widget.uniqueId, style: TextStyle(fontSize: 22.0, color: Colors.red), ), alignment: AlignmentDirectional.center, ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(30.0), color: Colors.yellow, child: Text( 'open flutter page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ) Container( height: 300, width: 200, child: Text( '', style: TextStyle(fontSize: 22.0, color: Colors.black), ), ) ], ))), ); } }
然后,运行项目,就可以从原生页面跳转到Flutter页面,如下图所示效果。
2.2 iOS集成
和Android的集成步骤一样,使用Xcode打开原生iOS工程,然后在iOS的AppDelegate文件中初始化Flutter Boost ,如下所示。
@interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { MyFlutterBoostDelegate* delegate=[[MyFlutterBoostDelegate alloc ] init]; [[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) { } ]; return YES; } @end
下面是自定义的FlutterBoostDelegate的代码,如下所示。
@interface MyFlutterBoostDelegate : NSObject<FlutterBoostDelegate> @property (nonatomic,strong) UINavigationController *navigationController; @end @implementation MyFlutterBoostDelegate - (void) pushNativeRoute:(FBCommonParams*) params{ BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; UIViewControllerDemo *nvc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]]; if(present){ [self.navigationController presentViewController:nvc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:nvc animated:animated]; } } - (void) pushFlutterRoute:(FBCommonParams*)params { FlutterEngine* engine = [[FlutterBoost instance ] getEngine]; engine.viewController = nil; FBFlutterViewContainer *vc = FBFlutterViewContainer.new ; [vc setName:params.pageName params:params.arguments]; BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; if(present){ [self.navigationController presentViewController:vc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:vc animated:animated]; } } - (void) popRoute:(FBCommonParams*)params result:(NSDictionary *)result{ FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController; if([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: params.uniqueId]){ [vc dismissViewControllerAnimated:YES completion:^{}]; }else{ [self.navigationController popViewControllerAnimated:YES]; } } @end
如果要在原生iOS代码中打开或关闭Flutter页面,可以使用下面的方式。
[[FlutterBoost instance] open:@"flutterPage" arguments:@{@"animated":@(YES)} ]; [[FlutterBoost instance] open:@"secondStateful" arguments:@{@"present":@(YES)}];
三、Flutter Boost架构
对于混合工程来说,原生端和Flutter端对于页面的定义是不一样的。对于原生端而言,页面通常指的是一个ViewController或者Activity,而对于Flutter来说,页面通常指的是Flutter组件。FlutterBoost框架所要做的就是统一混合工程中页面的概念,或者说弱化Flutter组件对应容器页面的概念。换句话说,当有一个原生页面存在的时候,FlutteBoost就能保证一定有一个对应的Flutter的容器页面存在。
FlutterBoost框架其实就是由原生容器通过消息驱动Flutter页面容器,从而达到原生容器与Flutter容器同步的目的,而Flutter渲染的内容是由原生容器去驱动的,下面是Flutter Boost 给的一个Flutter Boost 的架构示意图。
可以看到,Flutter Boost插件分为平台和Dart两端,中间通过Message Channel连接。平台侧提供了Flutter引擎的配置和管理、Native容器的创建/销毁、页面可见性变化通知,以及Flutter页面的打开/关闭接口等。而Dart侧除了提供类似原生Navigator的页面导航接口的能力外,还负责Flutter页面的路由管理。
总的来说,正是基于共享同一个引擎的方案,使得FlutterBoost框架有效的解决了多引擎的问题。简单来说,FlutterBoost在Dart端引入了容器的概念,当存在多个Flutter页面时,FlutterBoost不需要再用栈的结构去维护现有页面,而是使用扁平化键值对映射的形式去维护当前所有的页面,并且每个页面拥有一个唯一的id
四、FlutterBoost3.0更新
4.1 不入侵引擎
为了解决官方引擎复用引起的问题,FlutterBoost2.0拷贝了Flutter引擎Embedding层的一些代码进行改造,这使得后期的升级成本极高。而FlutterBoost3.0采用继承的方式扩展FlutterActivity/FlutterFragment等组件的能力,并且通过在适当时机给Dart侧发送appIsResumed消息解决引擎复用时生命周期事件错乱导致的页面卡死问题,并且,FlutterBoost 3.0 也兼容最新的官方发布的 Flutter 2.0。
4.2 不区分Androidx和Support分支
FlutterBoost2.0通过自己实现FlutterActivityAndFragmentDelegate.Host接口来扩展FlutterActivity和FlutterFragment的能力,而getLifecycle是必须实现的接口,这就导致对androidx的依赖。这也是为什么FlutterBoostView的实现没有被放入FlutterBoost3.0插件中的原因。而FlutterBoost3.0通过继承的方式扩展FlutterActivity/FlutterFragment的能力的额外收益就是,可以做到不依赖androidx。
4.3 双端设计统一,接口统一
很多Flutter开发者只会一端,只会Android 或者只会IOS,但他需要接入双端,所以双端统一能降低他的 学习成本和接入成本。FlutterBoost3.0,在设计上 Android和IOS都做了对齐,特别接口上做到了参数级的对齐。
4.4 支持 【打开flutter页面不再打开容器】 场景
在Flutter模块内部,Flutter 页面跳转Flutter 页面是可以不需要再打开Flutter容器的,不打开容器,能节省内存开销。在FlutterBoost3.0上,打开容器和不打开容器的区别表现在用户接口上仅仅是withContainer参数是否为true就好。
InkWell( child: Container( color: Colors.yellow, child: Text( '打开外部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ), InkWell( child: Container( color: Colors.yellow, child: Text( '打开内部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", withContainer: true, arguments: <String, String>{'from': widget.uniqueId}), )
4.5 生命周期的精准通知
在FlutterBoost2.0上,每个页面都会收到页面生命周期通知,而FlutterBoost3.0只会通知页面可见性实际发生了变化的页面,接口也更符合flutter的设计。
4.6 其他Issue
除了上面的一些特性外,Flutter Boost 3.0版本还解决了如下一些问题:
- 页面关闭后参数的传递,之前只有iOS支持,android不支持,目前在dart侧实现,Ios 和Android 都支持。
- 解决了Android 状态栏字体和颜色问题。
- 解决了页面回退willpopscope不起作用问题。
- 解决了不在栈顶的页面也收到生命周期回调的问题
- 解决了多次setState耗性能问题。
- 提供了Framgent 多种接入方式的Demo,方便tab 场景的接入。
- 生命周期的回调代码,可以用户代码里面with的方式接入,使用更简单。
- 全面简化了,接入成本,包括 dart侧,android侧和ios
- 丰富了demo,包含了基本场景,方便用户接入 和测试回归
到此这篇关于Flutter Boost 混合开发框架的文章就介绍到这了,更多相关Flutter Boost内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!,希望大家以后多多支持我们!