本文由企业微信客户端团队黄玮分享,原题“在流沙上筑城:企微鸿蒙开发演进”,下文进行了排版优化和内容修订。
1、引言
当企业微信团队在2024年启动鸿蒙Next版开发时,我们面对的是双重难题:
- 1)在WXG小团队模式下,如何快速将数百万行级企业应用移植到全新操作系统?
- 2)在鸿蒙API 还是Preview的初期,如何保持业务代码的稳定,在API快速更新的浪潮中岿然不动?
DataList框架给出了破局答案(即通过三重机制构建数字负熵流):
- 1)结构化熵减:将业务逻辑渲染到UI的过程抽象为数据流,使鸿蒙与Android共享同一套数据驱动的开发机制;
- 2)动态熵减:通过抽象出来的UI数据层屏蔽鸿蒙API的变化,让业务代码历经三个版本的UI层大改而不受影响;
- 3)认知熵减:将跨平台差异封装为一系列通用组件,降低开发者心智负荷,可以专注于业务开发而不用关心技术变更。
本文将要分享的是企业微信的鸿蒙Next客户端架构的演进过程,面对代码移植和API不稳定的挑战,提出了DataList框架解决方案。通过结构化、动态和认知三重熵减机制,将业务逻辑与UI解耦,实现数据驱动开发。采用MVDM分层架构(业务实体层、逻辑层、UI数据层、表示层),屏蔽系统差异,确保业务代码稳定。
技术交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4812-1-1.html)
2、企业微信客户端框架进化史
罗马不是一天建成的,我们在开发框架方面,也经历了 发现问题、探索方案 、优化改进 的过程。
野蛮生长(2019年前):
- 1)背景:团队缺乏统一规范,开发风格各异;
- 2)问题:相同功能重复实现,维护成本高。
初步探索(2019-2022):
- 1)背景:急需统一开发范式,提高开发效率;
- 2)实现:EasyList框架,提出"一切皆列表"理念,封装模板代码,让开发者专注于业务开发;
- 3)问题:未严格隔离业务与UI,退化为MVC模式;抽象能力不足,组件复用率极低。
渐入佳境(2022-2024):
- 1)创新:实现了基于数据驱动/分层隔离的DataList框架;
- 2)价值:框架提供抽象能力,降低开发认知负担;让每一个组件都具备复用能力,极大提高了复用率,助力通用组件从个位数突破至50+。
3、企业微信客户端框架整体设计
3.1 整体架构设计
DataList是一套基于数据驱动的分层隔离框架,整体架构图如下图所示。
▲ 图1:DataList MVVM架构图
接下来将从数据流向、分层架构的角度分别对这张图进行讲解。
3.2 数据流向设计
从数据流向的角度,DataList框架可以简单分为Data/List两部分:
- 1)List:业务逻辑部分,简单来说就是业务数据如何转换为UI数据;
- 2)Data:数据驱动部分,UI数据如何渲染为实际的UI/如何驱动UI刷新。
▲ 图2:DataList数据流向图
3.3 MVDM环形分层设计
DataList通过将业务数据到UI数据的转换逻辑独立出来,系统形成了清晰的边界层次:
- 1)业务实体层(Repo):负责请求数据,拿到业务数据(保持稳定);
- 2)业务逻辑层(ViewModel):处理业务逻辑,负责业务数据到UI数据的转换(保持稳定);
- 3)UI数据层(CellData/ViewData):对UI层的抽象(内部适应变化,对外接口稳定);
- 4)表示层(Cell):处理具体UI渲染(拥抱变化,适配平台新特性)。
相当于MVVM(Model-View-ViewModel)变成了MVDM(Model-View-Data-ViewModel)。
箭头代表依赖指向:
▲ 图3:DataList环形分层图
这里介绍下UI数据层。
将整个控件数据化,即为ViewData:
export class TextData extends BaseData {
text?: string | Resource
fontColor?: ResourceColor
fontSize?: number | string | Resource
fontWeight?: number | FontWeight | string
将多个ViewData组合起来,成为一个组件CellData:
//由Image+Text组成
export class ImgTextCellData extends BaseCellData {
builder: WrappedBuilder = wrapBuilder(ImgTextCellBuilder)
root: RowData
img?: ImgData //对应Image控件
text?: TextData //对应Text控件
}
由于CellData内不含任何业务代码,所以不受限于业务,天然可以复用。下图是组件复用统计(现有58个组件,数千次复用)。
▲ 图4:通用组件复用统计
这样分层的好处:
- 1)方便UI大规模复用;
- 2)跨平台代码一致性;
- 3)隔离业务与UI,UI层变动不影响业务逻辑。
3.4 无可删减:DataList开发示例
完美的达成,不在于无可增添,而在于无可删减。 ——《风沙星辰》 安托万·德·圣-埃克苏佩里
梳理一下,开发一个业务需求,哪些部分是无可删减的?
其实就是业务相关的部分:
- 1)数据请求;
- 2)业务数据转为UI(UI数据)。
这些都是必须由开发者填写的逻辑,这些步骤框架最多只能简化,不能代劳。
比如:我们开发一个极简版本的人员列表,看下对应步骤。
数据请求:
//Repo对应Model层
class DemoContactRepo():IListRepository {
override fun requestData(
req: DemoContactReq,//请求参数
callback: (rsp: DemoContactRsp) -> Unit,//结果回调
errorCallback: (errorCode: Int, errorMsg: Any?) -> Unit//错误回调
) {
//请求数据,返回
ContactService.getContact(req){contacts->
callback(contacts)
}
}
}
数据转换:
//继承自单数据源列表基类,泛型指明请求与返回的业务数据类型
class DemoContactViewModel: SingleListViewModel() {
/**
* 业务数据转为UI数据
*/
overridefun transferData(data: DemoContactRsp): List {
returndata.contacts.map {
ImgPhotoTextImgCellData( //通用组件
dataId = it.id,
photo = PhotoData(url = it.avatar),//一个图片控件
leftText = TextData(text = it.name))//一个文本控件
}
}
/**
* 拉取数据所用的仓库(对应Model层)
*/
overridefun initRepository(): IListRepository {
return DemoContactRepo()
}
/**
* 初次或刷新页面时的请求参数
*/
overridefun refreshParam(arguments: Bundle?): DemoContactReq {
return DemoContactReq(0,20)
}
}
算上注释,「总计39行」,一个极简版联系人列表就开发完成了。
▲ 图5:DataList联系人 Demo
如果是一个本地静态页面,可以去掉网络请求部分,直接堆砌通用组件(CellData)即可,完整代码只要40行。
//继承自本地静态列表基类,无数据请求
class DemoAttendanceViewModel ocalSingleListViewModel() {
//...
// |