Clean 架构下的现代 Android 架构指南

Clean 架构下的现代 Android 架构指南

Clean 架构是 Uncle Bob 提出的一种软件架构,Bob 大叔同时也是 SOLID 原则的命名者。

Clean 架构图如下:


这张图描述的是整个软件系统的架构,而不是单体软件,其中至少包括服务端以及客户端。

对于 Android 单体应用开发来说应该还需要一个更贴切更精确的 Clean 架构图。

我大概总结了一下过往的开发经验,找出了应用架构中的重要部分,然后绘制了下面这张 Clean 架构指导下的 Android 应用架构图:

以及一般数据流向图:

依赖关系

Clean 架构基本准则是源码级别的内层不依赖外层,依赖关系永远是单向的,外层向内层依赖。

如上,Model 层是没有任何依赖的,UseCase 可以依赖 Model 和 Repo 等,但绝不能依赖 ViewModel,UI 层依赖 ViewModel,但 ViewModel 绝不能依赖 UI 层。

为了达到这种源码级别的依赖关系,我们必须借助一些工具来实现依赖注入,一般可以使用 Hilt 或者 Koin 这样的框架来实现。

另外,依赖注入不应该被滥用,不是所有的对象都适合用依赖注入,只有那些有明确层次关系的模块,互相有着明确的依赖关系的才需要。对于一些工具类,显然是没必要注入的。

Model(领域模型)

业务模型,或者叫领域模型,是根据软件业务设计出来的具体模型,一般来说会是个 data class,其中不包含任何业务逻辑,只是个单纯的模型对象。

由于是在整个架构的最内层,所以不依赖任何其他模块,并且相对稳定,设计的时候需要考虑这点。如果模型发生变化,那意味着整个上层的依赖方都可能发生变化,需要重新测试。

在命名和包结构上,领域模型不需要带 Entity 之类的后缀,直接命名为像 User 一样即可,但考虑到这是在软件的最内层,可能会被所有模块依赖到,所以要尽可能贴近其设计目标,并且不能太过宽泛。在包结构上,需要被存放在 model 包下面。

Adapter(数据适配器)

数据适配器层主要用来做数据转换,主要有两个职责:

  • 转换网络接口实体数据类和领域模型。
  • 领域模型之间的互相转换。

Adapter 层也比较纯粹,只负责简单的数据转换,而且对外暴漏的函数都是幂等函数

如果数据转换过程中涉及到复杂的业务逻辑,可以考虑先用 UseCase 处理完成后再交给 Adapter。但因为 Adapter 层比 UseCase 层更靠内,所以 Adapter 不能依赖 UseCase。

习惯上,我们会以待转换类为开头,Adapter 结尾命名,例如我们要把 UserEntity 转换为 User, 那么应该这么写:

class UserEntityAdapter{fun toUser(entity: UserEntity): User {//...}
}

Repo

对于我们 Android 开发来说,Repo 层应该是对网络接口或本地磁盘的数据读写的封装,对于 Repo 的使用者来说,不需要关注具体的实现,且 Repo 中一般不具备复杂的业务逻辑,只能包含简单的数据处理逻辑。

Repo 应当隐藏具体的实现细节,不仅包括获取方式是网络还是本地数据,也应该隐藏对应的实体数据类,这意味着 **Repo 层对外暴漏的函数的入参和出参不能包含接口返回的实体类,也不应该包含数据库表实体类,只能包含领域模型或者基本类型。**我们给 Room 设计的数据库表的 data class 应该限制在 Repo 内部,我们给 Retrofit 设计的接口返回数据 data class 也同样应该限制在 Repo 内部。

data class UserEntity(val name: String, val avatar: String)interface UserService {@GET("/user")suspend fun getUserInfo(@query("id") id: String): UserEntity
}data class User(val name: String, val avatar: String)class UserEntityAdapter @Inject constructor() {fun toUser(entity: UserEntity): User {return User(name = entity.name, avatar = entity.avatar)}
}class UserRepo @Inject constructor(private val userEntityAdapter: UserEntityAdapter,
) {private val userService: UserService by lazy {retrofit.create(UserService::class.java)}suspend fun getUser(id: String): User {return userService.getUserInfo(id).let(userEntityAdapter::toUser)}
}

除了上面说的相应数据的转换之外,请求数据也需要在 Repo 层转换,对于 Post 请求来说,可能会存在一个请求实体,这个实体数据类最好也不要对外暴漏,可以在 Repo 层的请求方法入参那里做一些转换,最好能让入参更简单友好。

Repo 层还有一个作用就是负责把从接口或者数据库中出来的不友好的数据模型转换成友好的数据模型

另外,现在由于有了 BFF 的存在,在某些比较简单的业务场景下我们可以为了方便做一些妥协,也就是接口的响应数据实体类可以穿透 Repo 层,直接给到 ViewModel,甚至是 UiState 使用,但应该明白这只是为了方便的妥协,并不是最佳实践,需要严格控制影响范围

UseCase(用例)

UseCase 一般是指特定应用场景下的业务逻辑,用例引导了数据在模型之间的输入输出,并且指挥着业务实体利用其中的关键业务逻辑来实现用例的设计目标。

因此,一个 UseCase 往往只包含一段具体的业务逻辑,他的输入是基本类型或者领域模型,输出也是,并且是幂等函数,也就是纯函数,所以 Google 建议我们每个 UseCase 只包含一个公开的函数,类似于下面这种写法:

class DoSomethingUseCase {operator fun invoke(xxx: Foo): Bar {// ...}
}

通过利用 Kotlin 特性来使 UseCase 在使用的时候达到直接使用函数的体感。

但考虑到依赖以及管理问题,UseCase 最好还是不要直接使用函数来实现,应当按照上面的方式,定义一个类,然后再暴露一个通过操作符重载的函数。

在使用 UseCase 时可以这么用:

class LoginViewModel @Inject constructor(private val doSomething: DoSomethingUseCase,
): ViewModel(){fun onLoginClick(){doSomething()}
}

UseCase 的问题

UseCase 的粒度非常细,基本上每个 UseCase 就是一个函数,在复杂的业务背景下将会存在非常多的 UseCase,随着业务的增加,对他们的管理将难以为继。

因此,UseCase 需要一个有效的手段来进行管理,首先,应当按功能对他们的包名进行划分。同一个业务的 UseCase 最好具备相同的包名。

其次,我们不能陷入所有业务都用 UseCase 的极端情况中,很多时候,我们可以将一些极度类似的功能组织在一个类中,其中提供多个公开的方法,这样的写法在以前很常见,比如各种 Manager, Helper, Resolver 等,他们能有效的减少 UseCase 数量,并且相对简单。

UiState

UiState 是用来描述当前 UI 状态的集合类,一般来说应该是个 data class。

UiState 一定是不可变类,如果希望更改其中的某个值,应当重新创建一个对象,直接通过 data class 提供的 copy 方法即可,例如:

data class LoginUiState(val name:String,val avatar: String,val consentAgreed: Boolean
)fun onAgreeChecked(){uistate = uiState.copy(consentAgreed = true,)
}

UiState 中的数据应当尽可能的方便给 UI 直接使用,因为 UiState 本身就是为了 UI 设计的,例如对于一个需要显示的格式化后的时间,格式化的逻辑最好放在 ViewModel 或者更内层,而不是直接给 UiState 一个时间戳,让 UI 层去格式化。很多时候看起来简单的逻辑也可能犯错误,UI 层没有能力处理异常。

在 ViewModel 中如果需要更新 UiState,可以直接通过 update 方法。

_uiState.update {it.copy(name = "zhangke")
}

ViewModel

ViewModel 负责管理 UI 状态,执行对应的业务逻辑。

因此 ViewModel 的生命周期与页面是一致的。

一般来说我们会通过直接使用 Jetpack 提供的 ViewModel,但也可以自己创建其他类型的 ViewModel,只要控制好生命周期即可。

ViewModel 主要负责两件事情:

  • 对外提供当前 UI 状态
  • 接收 UI 事件并作出响应

当前 UI 状态我们通过将 UiState 包装在 StateFlow 里对外提供。

private val _uiState = MutableStateFlow()
val uiState:StateFlow<UserUiState> = _uiState.asStateFlow()

接受 UI 事件这点需要注意,ViewModel 需要做的是接收 UI 事件,例如用户手势输入,至于用户点击之后要做什么事情这是 ViewModel 的内部逻辑,不应该对外暴露。

UI 层

我们这里说的 UI 层就是指一个页面,除了常规的 Activity/Fragment 之外,对于 Compose 来说一个页面可能对应的是一个 Composable 函数,这取决于 UI 层的实现。

UI 层应该完全是数据驱动的,UI 层的作用就是百分之百的将 UiState 渲染出来,UiState 发生变化,UI 也跟着变化,这一点声明式 UI 框架做的很好。

UI 层虽然也可以处理一些简单的事件,但大部分的事件都还是要交给 ViewModel 来处理。

以上就是 Android 整洁架构中的一些关键概念的介绍,我已经按照这个架构开发了一年多了,目前看下来确实会让架构很整洁,但对于一些复杂的业务场景,尤其是可能需要穿透多个层级,跨越常规生命周期的模块就需要更精细的设计了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/197159.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从声纹模型到语音合成:音频处理 AI 技术前沿 | 开源专题 No.45

facebookresearch/audiocraft Stars: 16.6k License: MIT AudioCraft 是一个用于音频生成的 PyTorch 库。它包含了两个最先进的 AI 生成模型 (AudioGen 和 MusicGen) 的推理和训练代码&#xff0c;可以产生高质量音频。该项目还提供了其他功能&#xff1a; MusicGen&#xf…

MySQL笔记-第02章_MySQL环境搭建

视频链接&#xff1a;【MySQL数据库入门到大牛&#xff0c;mysql安装到优化&#xff0c;百科全书级&#xff0c;全网天花板】 文章目录 第02章_MySQL环境搭建1. MySQL的卸载步骤1&#xff1a;停止MySQL服务步骤2&#xff1a;软件的卸载步骤3&#xff1a;残余文件的清理步骤4&am…

Appium:iOS部署

iOS部署 要部署Appium进行iOS自动化测试&#xff0c;需要遵循以下步骤&#xff1a; 1. 安装Node.js&#xff1a; 访问Node.js官网&#xff08;https://nodejs.org/download/&#xff09;&#xff0c;根据您的操作系统和需求下载合适的版本。在Windows系统上&#xff0c;安装…

原生横向滚动条 吸附 页面底部

效果图 /** 横向滚动条 吸附 页面底部 */ export class StickyHorizontalScrollBar {constructor(options {}) {const { el, style } optionsthis.createScrollbar(style)this.insertScrollbar(el)this.setScrollbarSize()this.onEvent()}/** 创建滚轴组件元素 */createS…

WEB渗透—反序列化(十一)

Web渗透—反序列化 课程学习分享&#xff08;课程非本人制作&#xff0c;仅提供学习分享&#xff09; 靶场下载地址&#xff1a;GitHub - mcc0624/php_ser_Class: php反序列化靶场课程&#xff0c;基于课程制作的靶场 课程地址&#xff1a;PHP反序列化漏洞学习_哔哩哔_…

问题集锦(一)

一、字符串常量池是什么&#xff1f;不同的JDK版本都分别位于哪个区域&#xff1f; 字符串常量池是Java语言中的一个特性&#xff0c;它是一个存储字符串常量的池子。字符串常量池的设计目的是为了减少重复的字符串对象&#xff0c;从而节约内存空间和提高程序性能。 在JDK1.…

Hadoop YARN组件

1. 请解释Yarn的基本架构和工作原理。 YARN&#xff0c;也被称为"Yet Another Resource Negotiator"&#xff0c;是Apache HadoopYARN&#xff0c;也被称为"Yet Another Resource Negotiator"&#xff0c;是Apache Hadoop的一部分&#xff0c;它被设计为一…

OWASP Web 安全测试指南-Web 应用程序安全测试

Web 应用程序安全测试 4.0 简介和目标 4.1 信息收集 4.2 配置和部署管理测试 4.3 身份管理测试 4.4 身份验证测试 4.5 授权测试 4.6 会话管理测试 4.7 输入验证测试 4.8 错误处理测试 4.9 弱密码测试 4.10 业务逻辑测试 4.11 客户端测试 4.0 简介和目标 本节介绍 O…

Python自动化测试web常见框架汇总

1、前言 目前&#xff0c;有非常多的Python框架&#xff0c;用来帮助你更轻松的创建web应用。这些框架把相应的模块组织起来&#xff0c;使得构建应用的时候可以更快捷&#xff0c;也不用去关注一些细节&#xff08;例如socket和协议&#xff09;&#xff0c;所以需要的都在框…

金融帝国实验室(Capitalism Lab)V10版本新增崭新企业总部大楼

金融帝国实验室&#xff08;Capitalism Lab&#xff09;V10版本新增崭新企业总部大楼 ————————————— 【全新V10版本开发播报】 即将推出的V10版本将引入两座崭新的企业总部大楼&#xff01;新大楼采用先进的现代化建筑设计&#xff0c;以取代旧的总部大楼。 ———…

[SHCTF 2023]——week1-week3 Web方向详细Writeup

Week1 babyRCE 源码如下 <?php$rce $_GET[rce]; if (isset($rce)) {if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\|\%|\>|\<|\|\"/i", $rce)) {system($rce);}else {echo "hhhhhhacker!!!".&…

代码随想录二刷 | 字符串 |重复的子字符串

代码随想录二刷 &#xff5c; 字符串 &#xff5c;重复的子字符串 题目描述解题思路 & 代码实现移动匹配KMP算法 题目描述 459.重复的子字符串 给定一个非空的字符串 s &#xff0c;检查是否可以通过由它的一个子串重复多次构成。 示例 1: 输入: s “abab” 输出: tru…

力扣二叉树--第三十八天

前言 后面几天准备期末考试&#xff0c;要断更了。8号or 9号再开始。 内容 一、二叉搜索树中的众数 501. 二叉搜索树中的众数 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c…

XC4060 40V降5V/3.3V 0.6A小电流高耐压芯片 适用于单片机供电输出、电池供电设备

XC4060器件是高效率&#xff0c;同步降压DC/DC稳压器。具有较宽的输入范围&#xff0c;它们适用于广泛的应用&#xff0c;例如来自非稳压源的功率调节。他们的特点是一个长距离(500mQ/300mQ2型) 内部开关的效率最高 (92%)。Sum od (非A选项)和PWM模式(A选项)&#xff0c;工作频…

大部分只使用这 14 个 Git 命令就够了

必须了解的命令整理 1&#xff0c;git init 初始化一个新的Git仓库。 这将在当前目录中创建一个名为".git"的子目录&#xff0c;Git会将所有仓库的元数据存储在其中。 2&#xff0c;git clone 克隆一个已存在的仓库。 这会创建一个本地仓库的副本&#xff0c;包…

【C语言】深入理解C语言中的数学运算和类型转换

文章目录 引言取负运算的奥秘源码探索分析与解读 浮点数运算的精细差异源码分析 精度损失与隐式类型转换精度和除零运算探究float类型和double类型的精度各是多少&#xff08;即十进制有效位的位数&#xff09;&#xff1f;在你的机器上&#xff0c;“负数开方”是如何处理的&a…

【Spring Boot】如何集成mybatis-plus

在pom文件中导入maven坐标 <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.23</version></dependency><!--mybatis-plus--><dependency><groupId>com.ba…

uniapp 连接斑马PDA调试

1、先把PDA设置成开发者模式 打开设置--》关于手机 --》单击版本号&#xff0c;5次以上 连线单击5次以上 2、后退--》找到系统 --》高级 3、打开 --》开发都模式 4、找到调试 --》 打开USB调试 5、设置USB偏好设置&#xff0c;插入电脑连接PDA就会在通知栏上显示&#xff0c;默…

蓝桥杯 动态规划

01 数字三角形 #include<bits/stdc.h> using namespace std; const int N105; using lllong long; ll a[N][N],dp[N][N]; int main(){int n;cin>>n;for(int i1;i<n;i){for(int j1;j<i;j){cin>>a[i][j];}}for(int i5;i>1;i--){for(int j1;j<i;j){…

拒绝废话,直接开画!Python零基础教程之画图

引文 很多教程&#xff0c;开始教python&#xff0c;就是语法呀&#xff0c;字符类型这些基础的&#xff0c;虽说是基础&#xff0c;你也不能说没用。 但是&#xff0c;对于前期要快速成长的我们来说&#xff0c;属实不够看。 我们是新手&#xff0c;我们是菜鸟&#xff0c;但…