为了账号安全,请及时绑定邮箱和手机立即绑定

开源|Fair在安居拍房App中的实践

标签:
Android Html5 iOS

 谢雄亮、王海君 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的性能。

现有架构

安居拍房App是采用三端分离的混合开发模式,Flutter产物会以AAR或者Framework的方式集成到Android和iOS原生项目中。

安居拍摄App主要是记录房源信息、拍摄房源图片和VR的功能,如何把现有的Flutter能力,改造成动态代码可调用功能,就需要把网络、权限管理、图片选择、VR拍摄等能力提前预埋,定好通信协议,以便后续动态模块可以正常使用。扩展Fair能力前的架构,如下所示:

https://img1.sycdn.imooc.com/62d5289a0001cdab10800764.jpg

能力预埋

如上一节所说,如何把一些平台能力提供给Fair动态调用,这部分工作需要提前规划和预埋。下面我们从完整界面路由预埋、动态和原始组合展示、已有组件和模版注册和第三方SDK能力扩展5个方面进行介绍。

2.1 完整界面

统一动态界面注册

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都可以实现灵活动态切换。如果项目允许,也可以推广这种方案。

2.2 界面部分元素

与整个界面的动态化相比,界面部分元素的动态化,在实际需求场景中遇到比较多。比如需要在原生列表中增加一种类型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(        // 原生内容      );    }  }

2.3 使用本地Widget组件

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'),            ],          )        ],      ),    );  }}

2.4 逻辑模版使用

由于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的时候注册给系统。

2.5 常用第三方SDK

关于第三方或者自定义插件的使用,在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;  }}

集成Fair后的架构

集成Fair动态化SDK后,重点需要考虑对未来能力的思考,把一些平台能力扩展能动态可使用的组件。这里主要包括插件、现有Widget(添加Annotation)和一些模版等能力预埋。扩展Fair能力后的架构,如下所示:

https://img1.sycdn.imooc.com/62d5291c00014eb910800749.jpg

部分效果展示

4.1 界面

https://img1.sycdn.imooc.com/62d5294d0001be9b07481284.jpg

如上图所示,得益于预先扩展了网络动态化支持和动态界面跳转,通过下发的Router协议可以很方便的构建一个完整的订单反馈界面。

4.2 首页列表动态Item

https://img3.sycdn.imooc.com/62d5297e0001a5e903730819.jpg

如上图所示,安居拍房App已经通过Flutter原生开发了一个首页列表,Fair除了支持把整个列表重新动态化,还支持一个更灵活的Item动态化。通过动态化的Item和点击后的动态界面Router跳转,很方便实现动态Item和进入的动态详情界面的功能。

版本管理

Fair是通过Fair Compiler工具对源Dart文件进行转化,生产动态产物的。不像MXFlutter、Kraken基于JS技术栈来实现动态化。项目一旦通过JS去实现,性能的损失是不可逆的,但是Fair就是基于Dart开发,可以在处理紧急需求时,通过动态转化,在正常发版时使用源代码即可。整个版本管理流程如下:

https://img2.sycdn.imooc.com/62d529b000011b1c10800434.jpg

性能数据

最后我们提供一下安居拍房App集成的一些性能数据,这个也是很多开发者关心的话题。后续Fair团队会提供,Fair与MXFlutter和Kraken的数据对比,敬请关后续的《Flutter动态化项目评测》。

6.1 测试环境和功能

Android

荣耀 v40 Android 10, 内存 8G

iOS

iPhoneXSMax,iOS 13.3, 内存 4G;

Flutter 引擎版本

1.17.3

测试界面

首页混合动态列表(如首页列表动态Item图)

6.2 包体积

Android

增大了13.2M。(我们默认使用两个常用的SO库v7a和v8a,如果只是用一个或者更多数据会有变化)

iOS

净增5.6M(arm64+armV7)

6.3 内存

因为安居拍房是混合开发的App,我们直接从集成Fair打包成AAR或者Farmework集成到原生后,通过Android Studio Profile 和Xcode Instruments 直接观察。

Android

净增20M

iOS

净增17.9M

6.4 启动时间

获取启动时间,我们并没有直接通过Dev Tools取直接获取数据,而是通过录屏截取从点击进入页面数据完整渲染之间的时间。

Android

净增0.05秒

iOS

净增0.1秒

6.5 帧率

界面加载完成后,在动态界面前后,快速滑动获取的数据,我们在Flutter环境时通过Dev Tools获取的数据。

Android

可忽略不计

iOS

可忽略不计

总结

安居拍房App 通过集成Fair获取了动态化的能力,目前项目已经上线并处理了几次小场景的动态化需求。在集成Fair后,建议大家能及时梳理出后续可能使用的动态化能力,比如常用的网络、权限、存储和图片选择等等,以免在使用时发现没有适配支持。Fair直接提供Widget级的动态化,无论在完整或者部分界面动态化使用场景都具备灵活性,建议大家使用。

谢谢大家!

交个朋友,帮我们点个star吧 🌟  😇:

Github地址:https://github.com/wuba/fair


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 1
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消