The Android Clean Architecture

前言

对于Android开发者,对MVP,MVVM,MVI架构都有所了解吧,也曾听过The Clean Architecture吧?
之前笔者都认为这几个架构是同一个目的不同实现,但后来发现其实不然。

  • Clean是一种架构风格,其核心思想就是关注点分离,强调应用程序不同部分应有明确的责任且相互独立,通过分层来提升代码可读性、可测试性和可维护性,针对于整个应用设计的。
  • MVP,MVVM,MVI这些则是架构模式,是应用程序对数据(或状态)和用户界面之间关系管理的一种解决方案(注:在app开发过程中,笔者个人是这么理解的)。

为什么笔者认为这些架构模式是应用程序对数据(或状态)和用户界面之间关系管理的一种解决方案?

相信大家都有过MVVM开发经验,在开发一个feature的时候,应该都有如下步骤:

  1. 通过Rest Api获取请求体并转换成Model(一般data class)
  2. ViewModel通过Repo获取得到Model,并根据情况转换或组合成UiState
  3. 在View上观察UiState并刷新界面
    以上步骤不管MVVM还是MVI都有类似。

在获得Model之前,不管经过Network还是LocalDB亦或组合成的数据的方式拿到Model,都不属于MVVM/MVI架构模式下的考虑范围。所以这些架构模式就是解决了传统Android开发过程中几乎所有业务逻辑代码都放在Activity中让人难以忍受的问题。

Clean架构在Android应用上的变体

Clean架构是Uncle Bob提出的一种软件设计架构,其架构图如下:

这是整个软件系统的架构图,对于Android应用,我们应该有更贴切的Clean架构图,参考nowinandroid项目和博客以及经验总结,Android应用程序的架构图大体如下:

通过了解Google对App architecture介绍,整理得如下数据流向图:

整体对应Android Clean架构分层,而在UI Layer中则使用所谓MVVM,MVI架构模式解决UI界面和业务逻辑数据的分离,并通过ViewModel连接两者。使用这种架构模式可以帮助开发人员更好管理和维护代码,更易于测试和扩展。

依赖关系

Clean架构的基本准则是层级之间,内层不依赖外层,依赖关系永远是从外向内的。

如上图,Model层不应该有任何依赖,Domain层可以依赖Repo或Model层,但不能依赖ViewModel层, UI层依赖ViewModel,但ViewModel不能依赖UI。

其中,Data层最终返回数据可以通过Mapper将Local DB的Entities或Remote Server返回的json转换成Model层的数据并提供外层使用。Domain层是可选择层,是否应该有取决于对业务场景的设计用例(UseCase)。ViewModel则是管理UiState。

Model层

业务模型或者领域模型,是根据业务设计出来的具体模型,一般来说是一个data class,其中不包含任何业务逻辑,仅仅是个单纯的模型对象。
由于是在整个架构的最内层,所以不依赖任何其他模块,应该稳定。所以设计时应该考虑,如果模型发生变化,则意味着整个上层依赖方都可能发生变化,需要重新设计。
也正因如此,之前笔者对于这层的把控总有些不自信,因为有些模型并不需要对所有层公开,但如果将其放到Model层则对有所其他层级都有影响(包括Gradle执行速度)。
所以根据Clean架构的基本准则,在设计Model的时候多考虑考虑,而Google也有所建议

Mapper层

也有很多人称之为Adapter(数据适配器)层,它介于Domain和Model层,包含于Repo层(也可以单独提取一层,但笔者认为没必要),主要有两个职责:

  • 网络返回的json数据/本地数据库Entities和领域模型之间的转换
  • 领域模型之间的相互转换

所以它相对纯粹,只负责数据转换,通常使用xxxMapper命名类 + 扩展函数,如:
UserEntityMapper.kt 文件中

fun User.toUserEntity(): UserEntity {
  return UserEntity(...)
}

fun UserEntity.toUser(): User {
  return User(...)
}

Repo层

如上数据流图所示,Repo是Data Layer中的一部分,是对网络接口数据或本地数据库的数据读写的封装,对于上层使用者Domain或ViewModel来说,不必关注其具体实现,使用即可。因此,Repo应当隐藏具体实现细节,这意味着Repo层对外暴露的函数的入参和出参不能包含接口返回的实体类,也不应该包含数据库表的Entity类,只能包含领域模型或基本类型,这里也体现了Mapper层的作用。


更多请看Clean 架构下的现代 Android 架构指南 | 开发者说·DTalk详细,里面对每层的描述讲解的都非常到位和详细。

但也要注意这些都不是一成不变的,开发过程中我们应该视业务需求而定,要有自己的思考。
比如我在开发一个多链支持的钱包应用时,就发送Token这个需求而言,大体流程都是如下:

graph TD
A(开始) -->|选择Token| B[输入Amount,收款地址,Memo等信息]
B --> C[Fee选择]
C --> D[签名交易]
D --> E{确认并广播交易}
E -->|成功| F[成功界面]
E -->|失败| G[失败界面]

但是在开发过程中,你会发现由于不同链需要的数据不一致所致,随着支持的链越来越多,在使用同一个导航界面实现的情况下,UiState和Model也会随之增大,代码中也许会出现大量的if elsewhen判断,对于维护和扩展是个很大的问题。
在重构的过程中,对Token统计并执行业务分类,将发送流程分为EVM、Bitcoin、Cosmos等,在选择Token的时候便走向不同的导航页面。而对于那些共同属性(如hash,amount,recipient,sender等)用接口包装分别实现,保留了部分共性避免没必要的重复代码。

流程还是如上图,但是我们在输入之前通过判断导航到不同UI界面,从而选择了不同的领域模型和业务处理能力的Repo。而没有在同一个ViewModel中持有所有链相关的Model和业务处理能力的Repo。

参考链接

What is the Clean Architecture?
App architecture
Clean 架构下的现代 Android 架构指南 | 开发者说·DTalk