谢雄亮、王海君 58技术
● 项目名称:Fair 2.0
● Github地址:https://github.com/wuba/fair
● 项目简介:Fair是为Flutter设计的动态化框架,可以通过Fair Compiler工具对Dart源文件的转化,使项目获得动态更新Widget的能力。Fair 2.0是为了解决 Fair 1.0版本的“逻辑动态化”能力不足。
随着今年政府对互联网的监管,在不少时候一个紧急需求只给1~2天整改上线,而且整改过程中需求也不是很明确,相关部门也不会给一个详细的需求文档让我们去开发,大家都是“猜测”需求的内容。在这种场景下,如果App具备动态更新的能力,会给公司减少很大的成本。面对需求不确定和紧急修改页面部分元素的能力,给予了动态化最合适的使用场景,而不只是Fix几个BUG。
Fair在58集团内的部分Flutter App中已经落地,终使集成Fair后的App获得了动态化的能力。以下文章内容主要以安居拍房App为例,介绍集成Fair的架构、业务场景所需的能力预埋,以及如何进行原生和动态化代码的维护,持续发挥Flutter的性能。
1 现有架构
安居拍房App是采用三端分离的混合开发模式,Flutter产物会以AAR或者Framework的方式集成到Android和iOS原生项目中。
安居拍摄App主要是记录房源信息、拍摄房源图片和VR的功能,如何把现有的Flutter能力,改造成动态代码可调用功能,就需要把网络、权限管理、图片选择、VR拍摄等能力提前预埋,定好通信协议,以便后续动态模块可以正常使用。扩展Fair能力前的架构,如下所示:
2 能力预埋
如上一节所说,如何把一些平台能力提供给Fair动态调用,这部分工作需要提前规划和预埋。下面我们从完整界面路由预埋、动态和原始组合展示、已有组件和模版注册和第三方SDK能力扩展5个方面进行介绍。
FairApp( child: MaterialApp( home: ***, routes: { *** // Fair动态页面跳转 'fair_page': (context) => FairWidget( name: _getParams(context, 'name'), path: _getParams(context, 'path'), data: {'fairProps': jsonEncode(_getData(context, _getParams(context, 'name')))}), }, ), )
如上所示,Fair的界面调用统一注册在routes里面的fair_page来跳转,根据传入的path和参数来完成对应的动态界面的展示。
// 动态界面Navigator.pushNamed(context, 'fair_page', arguments: { 'name': '动态界面 **', 'path': 'assets/bundle/lib_src_page_logic-page_sample_logic_page.fair.json', 'data': {"fairProps": {'pageName': '动态界面 **', '_count': 58}} });
如上所示,跳转到动态界面我们使用Navigator.pushNamed来完成。这里有同学可能会问,一个原生界面不是早把跳转的方式固定写好了吗?这里得益于安居拍房App的Api动态路由的设计,在一个原生界面中,点击跳转的路由都是后端下发的,App根据Api返回路径完成目标界面的跳转。看到这里大家就明白了,Api路由管理除了方便A/B Test,以至于原生与H5、RN、Flutter都可以实现灵活动态切换。如果项目允许,也可以推广这种方案。
与整个界面的动态化相比,界面部分元素的动态化,在实际需求场景中遇到比较多。比如需要在原生列表中增加一种类型item,Fair提供了FairWidget,方便跟原生组合显示。下面我们以在列表中预埋一个动态Item为例:
// 列表ListView.builder( padding: EdgeInsets.only(left: 20, right: 20), itemCount: _response.list.length, itemBuilder: (BuildContext context, int position) { return getItem(_response.list[position]); }))); // item 构建Widget getItem(var item) {// 根据后端item类型,选择是动态item还是原生item if (item.type == 'fair') { // 动态内容 return Container( alignment: Alignment.centerLeft, color: Colors.white, constraints: BoxConstraints(minHeight: 80), child: FairWidget( name: item.id, path: 动态资源名, data: {**参数**}); } else { return Column( // 原生内容 ); } }
Fair除了在Widget文件头部增加@FairPatch()来实现整个界面的动态化转化,还提供了@FairBinding()注解来实现本地Widget注册成动态可使用的组件。
本地Widget转化
// 一个本地Widget界面,提供给界面动态时使用@FairBinding()class CardWidget extends StatelessWidget { String text; CardWidget({this.text}); @override Widget build(BuildContext context) { return Text( text, style: TextStyle(color: Colors.red), ); }}
编译&注册
// flutter pub run build_runner build 后注册到FairApp中FairApp(child: MyApp(), generated: AppGeneratedModule());
动态界面中使用
@FairPatch()class CardWidgetState extends State<CardWidget> { @override Widget build(BuildContext context) { return Container( color: Colors.yellow, child: Column( children: [ Row( children: [ CardWidget(text: 'card 1'), ], ) ], ), ); }}
由于Fair对原生Flutter类型的支持有限,同时为了避免高频的Dart与JS的通信,我们一般会考虑把算法和交互流程一致的代码,做成固定模版,只把显示相关的部分做成动态的。安居拍房App首页的就是一个任务列表,而且考虑到后续列表的使用场景比较多,我们需要预埋一个逻辑模版,方便后续动态列表的生成。Fair提供了Delegate方便我们做模版扩展,例如下面的下拉刷新列表:
class ListDelegate extends FairDelegate { // 注册列表的构建方法 @override Map<String, Function> bindFunction() { var functions = super.bindFunction(); functions.addAll({ '_itemBuilder': _itemBuilder, '_onRefresh': _onRefresh, }); return functions; } // 通知JS侧 访问新的数据 Future<void> _onRefresh() async { await runtime?.invokeMethod(pageName, '_onRefresh', null); } // 得益于Fair是提供的第一层Widget Tree的组合,FairWidget可以完成动态的Widget的生成 Widget _itemBuilder(context, index) { var result = runtime?.invokeMethodSync(pageName, '_onItemByIndex', [index]); return FairWidget( name: itemData, path: '***', data: {'**'})}, ); }}
如上代码所示,像_onRefresh方法由DSL中注册到Flutter ListView,ListView构建回调会自动访问到此方法,于是我们可以使用这些回调方法做一层跟JS侧的通信,来完成界面的数据更新和Item内容的动态展示。
FairApp( delegate: { 'ListLoadMore': (ctx, _) => ListDelegate(), }, child: MaterialApp( home: *** ), )
如上所示,我们只需要把开发好的模版,注册到delegate中即可在DSL构建ListView的时候注册给系统。
关于第三方或者自定义插件的使用,在FlutterApp中非常常用。安居拍房App几乎每个界面都需要使用网络,而且由于App的使用场景,拍摄和权限功能,也是必须要提前预埋,方便后续动态化界面的使用。下面我们以权限插件为例,如何扩展提供给动态场景使用。
/// Fair 定义了第三方插件扩展的标准接口,开发者只需实现接口就可以使用底层的JS标准通信,这对开发者来说是无感知的class WBPermission extends IFairPlugin { Future<dynamic> requestPermission(map) async { // 根据从JS侧获得的map参数做具体的内容桥接 // 源Permission的状态获取 isGranted = await Permission.photos.request().isGranted; return Future.value(); } @override Map<String, Function> getRegisterMethods() { // 注册JS可调用的方法 var functions = <String, Function>{}; functions.putIfAbsent('requestPermission', () => requestPermission); return functions; }}
3 集成Fair后的架构
集成Fair动态化SDK后,重点需要考虑对未来能力的思考,把一些平台能力扩展能动态可使用的组件。这里主要包括插件、现有Widget(添加Annotation)和一些模版等能力预埋。扩展Fair能力后的架构,如下所示:
4 部分效果展示
如上图所示,得益于预先扩展了网络动态化支持和动态界面跳转,通过下发的Router协议可以很方便的构建一个完整的订单反馈界面。
如上图所示,安居拍房App已经通过Flutter原生开发了一个首页列表,Fair除了支持把整个列表重新动态化,还支持一个更灵活的Item动态化。通过动态化的Item和点击后的动态界面Router跳转,很方便实现动态Item和进入的动态详情界面的功能。
5 版本管理
Fair是通过Fair Compiler工具对源Dart文件进行转化,生产动态产物的。不像MXFlutter、Kraken基于JS技术栈来实现动态化。项目一旦通过JS去实现,性能的损失是不可逆的,但是Fair就是基于Dart开发,可以在处理紧急需求时,通过动态转化,在正常发版时使用源代码即可。整个版本管理流程如下:
6 性能数据
最后我们提供一下安居拍房App集成的一些性能数据,这个也是很多开发者关心的话题。后续Fair团队会提供,Fair与MXFlutter和Kraken的数据对比,敬请关后续的《Flutter动态化项目评测》。
荣耀 v40 Android 10, 内存 8G
iPhoneXSMax,iOS 13.3, 内存 4G;
1.17.3
首页混合动态列表(如首页列表动态Item图)
增大了13.2M。(我们默认使用两个常用的SO库v7a和v8a,如果只是用一个或者更多数据会有变化)
净增5.6M(arm64+armV7)
因为安居拍房是混合开发的App,我们直接从集成Fair打包成AAR或者Farmework集成到原生后,通过Android Studio Profile 和Xcode Instruments 直接观察。
净增20M
净增17.9M
获取启动时间,我们并没有直接通过Dev Tools取直接获取数据,而是通过录屏截取从点击进入页面数据完整渲染之间的时间。
净增0.05秒
净增0.1秒
界面加载完成后,在动态界面前后,快速滑动获取的数据,我们在Flutter环境时通过Dev Tools获取的数据。
可忽略不计
可忽略不计
7 总结
安居拍房App 通过集成Fair获取了动态化的能力,目前项目已经上线并处理了几次小场景的动态化需求。在集成Fair后,建议大家能及时梳理出后续可能使用的动态化能力,比如常用的网络、权限、存储和图片选择等等,以免在使用时发现没有适配支持。Fair直接提供Widget级的动态化,无论在完整或者部分界面动态化使用场景都具备灵活性,建议大家使用。
谢谢大家!
交个朋友,帮我们点个star吧 🌟 😇:
Github地址:https://github.com/wuba/fair