【NowInAndroid架构拆解】(1)分层设计与模块化

986 阅读7分钟

是故胜兵先胜而后求战,败兵先战而后求胜。——《孙子兵法·形篇》


【NowInAndroid架构拆解】系列文章


通过本文可以知道

  • 我为什么选择写架构主题
  • NowInAndroid的设计哲学
  • NowInAndroid的模块化设计

前言

为什么会有这一主题

架构系列是我近期的研究重点,在从事客户端研发工作十余年后,渐渐认识到业务开发只是表象,一百个项目就可能存在一百种业务页面。但在业务之下,良好的架构却是项目能够健康稳定发展的基石。客户端、前端作为用户最先接触到的软件部分,天然具有变化频繁的属性。但究其本质,要解决的是外在交互与内在代码之间的转化问题。《重构》、《代码整洁之道》、《代码大全》等一系列书籍,都在强调“高内聚低耦合”、“对扩展开放,对修改关闭”、“关注点分离”等等。

优秀的架构设计应该如同艺术品一般,让人赞叹其优美。就算做不到一劳永逸的完美,至少它也应当将美的部分与丑的部分隔离开,让在其上工作的工程师感到身心愉悦。

Android软件架构——窥一斑而知全豹

Android客户端开发技术发展至今,历经刀耕火种的MVC,初具雏形的MVP,另辟蹊径的MVVM,以及锦上添花的MVI,至今仍无法定论其架构设计是否已经到达终点。随着Jetpack Compose、Coroutine等新技术的普及,很难说未来会不会再出现一种新的架构思想。

软件架构的本质

软件架构(Software Architecture)本质上解决的是 复杂系统的结构性矛盾,其存在的意义在于为软件系统提供 可持续演进的骨架,是平衡技术、业务、团队和资源的关键决策框架。通过软件架构的结构化设计,能够有效地抵抗熵增,平衡技术业务和资源之间的不同诉求,以及为未来预留演进空间

NowInAndroid主要功能拆解

NowInAndroid (github.com/android/now…) 是Google官方最新架构设计Demo,应用了目前主流的模块化组件,且仍处于不断更新中。

image.png

整个APP可以看做新闻类应用,由3个Tab组成,分别是信息流、收藏、兴趣标签。首次进入APP时,需要选择感兴趣的主题(标签),在选择完成后同步刷新出该主题下的文章列表。可以对文章进行收藏,收藏后该文章出现在收藏Tab中。最后一个标签Tab与信息流Tab的数据是同步的。点击列表中的文章,会跳转到Chrome打开相应网页。

从主要功能推测其实现思路

  • 整体采用单一Activity架构,由3个Fragment组成。
  • 遵守单一数据源原则,至少有标签、文章、收藏三个数据源
  • 由数据驱动,可能采用了MVVM架构

接下来我们结合官方文档(见文末参考资料),对该APP的架构设计思路进行拆解。

NowInAndroid核心设计思路

分层与单向数据/控制流

image.png

  • 分为数据层、领域层(我更喜欢称之为功能层)、UI层
  • 数据自底向上单向流动,采用监听方式(虚线)
  • 控制由UI层向数据层单向流动,采用直接传递方式(实线)

应用启动->界面显示数据的数据控制流

image.png

  1. APP启动,将本地-服务器的数据同步任务加入WorkManager队列
  2. UI层发射Loading状态
  3. 离线Repo加载并发射用户数据
  4. 本地-服务器数据同步任务启动
  5. 从服务器下载最新的信息流
  6. 通过Retrofit请求服务器
  7. 服务器返回数据
  8. 将服务器数据写入数据库
  9. 完成数据库写入后,发射最新信息流数据
  10. 将信息流数据与用户数据融合,通过Repo发送融合后的数据至功能层(领域层)
  11. 领域层组合数据后,发射给UI
  12. UI状态变为Success,展现最新数据

数据层

根据“单一可信数据源”原则,采用本地数据库作为单一数据源,每次从网络加载最新数据后,会将数据先保存在DB中,保存成功后方能发射给UI。

通过suspend耗时函数直接写入数据库,通过Flow流向上层发射数据。

image.png

功能层(领域层)

功能层由用例(Use Case)组成,通过组合数据层Repo发来的不同数据,构成具体的业务场景数据结构,并发送Flow给上层UI。

功能层不包含任何事件处理的定义,理论上,可以把UI层替换为H5、iOS页面,而无需对功能层进行任何改动。

UI层

包括以下两部分:

  • 使用J etpack Compose 构建的 UI 元素
  • ViewModels

image.png

采用密封类(Sealed Class)封装了页面的状态(Loading、Success),ViewModel把功能层发送来的用例冷流进行组合,生成UI状态流,每一个UI状态流对应UI的相应展示规则,此状态流以热流形式发送。

为什么ViewModel底层是冷流而上层是热流?

  • 冷流具有懒加载属性,仅在开始订阅时启动加载数据,适用于网络、数据库,减少不必要的IO请求。
  • 热流具有实时性、共享数据源属性,适合展现UI的最新状态。

模块化拆分

模块化的好处

现在主流的大型APP均采用了模块化设计,其好处列举如下:

  • 可扩展性: 采用关注点分离原则,不同模块之间采用接口通信,内部实现可以自由替换升级。
  • 支持并行工作: 减少代码冲突,适合团队高效并行开发。
  • 职责明确: 每个模块有明确的责任人。
  • 封装/逻辑独立: 每个模块职责独立,关注点分离,便于阅读理解和测试。
  • 加快构建: 利用Gradle并行构建,减少构建时间。
  • 动态下发: 类似热更新,需要Google Play支持。
  • 可重用性: 提供了跨平台重用的可能。

模块化也有其负面效应,模块太多会导致项目复杂度上升,模块太少则起不到相应的解耦效果。对于简单的项目,单一模块反而更加适用。

NowInAndroid模块拆分

image.png

可见NIA大体上分为三层:

  • app模块: 由应用脚手架组成,将APP正常运行所需要的一切整合在一起,包含UI框架和应用级控制导航。它依赖全部feature和部分core。
  • feature模块: 功能层(领域层)模块,包括UI组件和ViewModels。feature彼此之间隔离,可以依赖底层的core。
  • core模块: 通用库模块,可以依赖于其它的core。图中可见只有commonmodel是不会依赖其它core的,它们位于依赖链的最底层。

需要注意,模块化的拆分不是一成不变的,并没有一个一劳永逸的正确答案。架构设计的目的之一,就是在满足现有需求的基础上,为将来可能发生的演变提前做兼容设计。

参考资料