Fair2.0专题系列
NO.7 Fair在安居客拍房App中的实践
● 项目名称:Fair 2.0
● Github地址:https://github.com/wuba/fair
● 项目简介:Fair是为Flutter设计的动态化框架,可以通过Fair Compiler工具对Dart源文件的转化,使项目获得动态更新Widget的能力。Fair 2.0是为了解决 Fair 1.0版本的“逻辑动态化”能力不足。
Fair2.0连载公告
关于Fair 2.0详细的设计和原理介绍,请关注后续文章,我们会以连载的方式,逐步公开架构设计原理。
文章开放频率为每周2篇,分别在周二和周四,敬请关注!
《Fair 下发产物-逻辑JS生成原理》
《Flutter 动态化项目评测》
随着今年政府对互联网的监管,在不少时候一个紧急需求只给1~2天整改上线,而且整改过程中需求也不是很明确,相关部门也不会给一个详细的需求文档让我们去开发,大家都是“猜测”需求的内容。在这种场景下,如果App具备动态更新的能力,会给公司减少很大的成本。面对需求不确定和紧急修改页面部分元素的能力,给予了动态化最合适的使用场景,而不只是Fix几个BUG。
Fair在58集团内的部分Flutter App中已经落地,终使集成Fair后的App获得了动态化的能力。以下文章内容主要以安居拍房App为例,介绍集成Fair的架构、业务场景所需的能力预埋,以及如何进行原生和动态化代码的维护,持续发挥Flutter的性能。
本篇文章主要介绍Fair在项目中不同场景的使用。具体的技术原理介绍,请查阅前面发布的《Fair逻辑动态化架构设计与实现》与《Fair 逻辑动态化通信实现》。
现有架构
安居拍摄App主要是记录房源信息、拍摄房源图片和VR的功能,如何把现有的Flutter能力,改造成动态代码可调用功能,就需要把网络、权限管理、图片选择、VR拍摄等能力提前预埋,定好通信协议,以便后续动态模块可以正常使用。扩展Fair能力前的架构,如下所示:
能力预埋
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界面,提供给界面动态时使用
()
class CardWidget extends StatelessWidget {
String text;
CardWidget({this.text});
Widget build(BuildContext context) {
return Text(
text,
style: TextStyle(color: Colors.red),
);
}
}
编译&注册
// flutter pub run build_runner build 后注册到FairApp中
FairApp(child: MyApp(), generated: AppGeneratedModule());
动态界面中使用
()
class CardWidgetState extends State<CardWidget> {
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();
}
Map<String, Function> getRegisterMethods() {
// 注册JS可调用的方法
var functions = <String, Function>{};
functions.putIfAbsent('requestPermission', () => requestPermission);
return functions;
}
}
集成Fair后的架构
部分效果展示
如上图所示,得益于预先扩展了网络动态化支持和动态界面跳转,通过下发的Router协议可以很方便的构建一个完整的订单反馈界面。
如上图所示,安居拍房App已经通过Flutter原生开发了一个首页列表,Fair除了支持把整个列表重新动态化,还支持一个更灵活的Item动态化。通过动态化的Item和点击后的动态界面Router跳转,很方便实现动态Item和进入的动态详情界面的功能。
版本管理
性能数据
荣耀 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获取的数据。
可忽略不计
可忽略不计
总结
安居拍房App 通过集成Fair获取了动态化的能力,目前项目已经上线并处理了几次小场景的动态化需求。在集成Fair后,建议大家能及时梳理出后续可能使用的动态化能力,比如常用的网络、权限、存储和图片选择等等,以免在使用时发现没有适配支持。Fair直接提供Widget级的动态化,无论在完整或者部分界面动态化使用场景都具备灵活性,建议大家使用。
有奖互动
Perseverance Prevails
点击阅读原文链接直接进入Github
请为我们点亮star吧!