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

news2025/1/16 8:51:52

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.coloradmin.cn/o/1294165.html

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

相关文章

探秘 JAVA 诞生之路,引领编程人生的宏伟著作

目录 一. 前言 二. Java 发展史 2.1. 1990年年末 2.2. 1991年 2.3. 1992年夏天 2.4. 1994年 2.5. 1995年 2.6. 1996年&#xff08;JDK1.0&#xff09; 2.7. 1997年&#xff08;JDK1.1&#xff09; 2.8. 1998年&#xff08;JDK1.2&#xff09; 2.9. 1999年&#xff0…

第十四章 : Spring Boot 整合spring-session,使用redis共享

第十四章 &#xff1a; Spring Boot 整合spring-session,使用redis共享 前沿 本文重点讲述&#xff1a;spring boot工程中使用spring-session机制进行安全认证&#xff0c;并且通过redis存储session&#xff0c;满足集群部署、分布式系统的session共享。 基于SPringBoot 2.3.2…

hive数据库查看参数/hive查看当前环境配置

文章目录 一、hive查看当前环境配置命令 在一次hive数据库执行命令 set ngmr.exec.modecluster时&#xff0c;想看一下 ngmr.exec.mode参数原先的值是什么&#xff0c;所以写一下本篇博文&#xff0c;讲一下怎么查看hive中的参数。 一、hive查看当前环境配置命令 set &#…

hadoop安装与配置-shell脚本一键安装配置(集群版)

文章目录 前言一、安装准备1. 搭建集群 二、使用shell脚本一键安装1. 复制脚本2. 增加执行权限3. 分发脚本4. 执行脚本5. 加载用户环境变量 三、启动与停止1. 启动/停止hadoop集群(1) 复制hadoop集群启动脚本(2) 增加执行权限(3) 启动hadoop集群(4) 停止hadoop集群(5) 重启hado…

算法:合并两个有序数组(双指针)

/*** param {number[]} nums1* param {number} m* param {number[]} nums2* param {number} n* return {void} Do not return anything, modify nums1 in-place instead.*/ var merge function(nums1,m,nums2,n) {let p1 m-1 let p2 n-1let p mn-1// 逆遍历while(p1 > 0…

前端开发常用的Vscode插件整理(持续更新)

本文记录用vscode进行前端开发时&#xff0c;常用到的有用的vscode插件&#xff0c;将不定时更新&#xff5e; 1、Chinese (Simplified) 将编辑器变成简体中文 2、vscode-icon 让 vscode 资源树目录加上图标&#xff0c;官方出品的图标库 3、Import Cost 引入包大小计算,对于…

深度学习——第3章 Python程序设计语言(3.3 Python数据类型)

3.3 Python数据类型 目录 1. Python数值数据类型 2. Python库的导入和使用 3. Python序列数据类型 4. Python组合数据类型 计算机能处理各种类型的数据&#xff0c;包括数值、文本等&#xff0c;不同的数据属于不同的数据类型&#xff0c;有不同的存储方式&#xff0c;支持…

使用Tomcat部署静态项目并处理BUG

--听讲的习惯 Tomcat介绍 tomcat what_Arenaschi的博客-CSDN博客 Tomcat安装及配置教程&#xff08;超详细&#xff09; 那些年我们用过的tomcat_Arenaschi的博客-CSDN博客 简单使用tomcat查看版本信息等_windows查看tomcat版本命令-CSDN博客 Tomcat部署html静态网站的五种方…

Android hook式插件化详解

引言 Android插件化是一种将应用程序的功能模块化为独立的插件,并动态加载到主应用程序中的技术。通过插件化,开发者可以将应用程序的功能分解成独立的模块,每个模块可以作为一个插件单独开发、测试和维护,然后通过动态加载的方式集成到主应用程序中,实现功能的动态扩展和…

骨传导耳机是怎么传声的?骨传导和入耳式哪个危害大一点?

先说结论&#xff0c;骨传导耳机通过人体骨骼来传递声音&#xff0c;骨传导和入耳式耳机&#xff0c;入耳式耳机的危害要大一些&#xff01; 一、骨传导耳机传声原理 骨传导耳机利用骨传导技术&#xff0c;通过将声音转化为机械振动信号&#xff0c;然后通过头骨、颌骨等头骨…

第二十一章——网络通信总结

网络程序设计基础 局域网与互联网 为了实现两台计算机的通信&#xff0c;必须用一个网络线路连接两台计算机。如下图所示 网络协议 1.IP协议 IP是Internet Protocol的简称&#xff0c;是一种网络协议。Internet 网络采用的协议是TCP/IP协议&#xff0c;其全称是Transmission…

基于Java SSM框架实现二手车交易网站系统项目【项目源码+论文说明】

基于java的SSM框架实现二手车交易网站系统演示 摘要 二手车交易网站采用B/S模式&#xff0c;促进了二手车交易网站的安全、质量、快捷的发展。传统的管理模式还处于手工处理阶段&#xff0c;管理效率极低&#xff0c;随着用户的不断增多&#xff0c;传统基于手工管理模式已经无…

ECharts的颜色渐变

目录 一、直接配置参数实现颜色渐变 二、使用ECharts自带的方法实现颜色渐变 一、两种渐变的实现方法 1、直接配置参数实现颜色渐变 横向的渐变&#xff1a; //主要代码 option {xAxis: {type: category,boundaryGap: false,data: [Mon, Tue, Wed, Thu, Fri, Sat, Sun]},yA…

国产化软件突围!怿星科技eStation产品荣获2023铃轩奖“前瞻优秀奖”

11月11日&#xff0c;2023中国汽车供应链峰会暨第八届铃轩奖颁奖典礼在江苏省昆山市举行。怿星科技凭借eStation产品&#xff0c;荣获2023铃轩奖“前瞻智能座舱类优秀奖”&#xff0c;怿星CEO潘凯受邀出席铃轩奖晚会并代表领奖。 2023铃轩奖“前瞻智能座舱类优秀奖” 铃轩奖&a…

倚天屠龙:Github Copilot vs Cursor

武林至尊&#xff0c;宝刀屠龙。号令天下&#xff0c;莫敢不从。倚天不出&#xff0c;谁与争锋&#xff01; 作为开发人员吃饭的家伙&#xff0c;一款好的开发工具对开发人员的帮助是无法估量的。还记得在学校读书的时候&#xff0c;当时流行CS架构的RAD&#xff0c;Delphi和V…

CDN是什么?对网站的作用大吗?

CDN即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络&#xff0c;依靠部署在各地的边缘服务器&#xff0c;通过中心平台的负载均衡、内容分发、调度等功能模块&#xff0c;使用户就近获取所需内容&#xff0c;降低网络拥塞&#xff0c;提高用户访问响应速度和命中率…

带你解锁Python操作文件的姿势

什么是文件 文件是计算机中用于存储数据的一种数据结构。它可以是文本文件、图像文件、音频文件、视频文件等等。文件由一系列字节组成&#xff0c;每个字节都有一个唯一的地址。文件可以在计算机的硬盘、固态硬盘、光盘等存储介质上存储&#xff0c;并且可以在需要时被读取和…

2024 年甘肃省职业院校技能大赛信息安全管理与评估赛项规程

2024 年甘肃省职业院校技能大赛高职学生组电子与信息大类信息安全管理与评估赛项规程 一、赛项名称 赛项名称&#xff1a;信息安全管理与评估 赛项类别&#xff1a;团体赛 赛项归属&#xff1a;电子与信息大类 二、竞赛目的 极安云科专注技能竞赛&#xff0c;包含网络建设…

CentOS系统中设置反向代理服务器的步骤

在CentOS系统中设置反向代理服务器可以帮助你隐藏原始服务器的细节&#xff0c;并提高服务器的安全性。以下是在CentOS系统中设置反向代理服务器的步骤概述&#xff1a; 安装反向代理软件&#xff1a; 常见的反向代理软件包括Nginx和Apache。你可以选择其中之一来作为你的反向…

Java的Font类createFont方法创建 +~JF 临时文件

一、问题背景 有一个创建图片的项目&#xff0c;每个图片都包含很多的文字&#xff0c;项目中需要生成海量的这类图片。在windows电脑上运行程序发现C盘的存储空间不断下降&#xff0c;直至为0。 二、问题定位 1、定位磁盘的问题文件 当C盘存储空间为0时&#xff0c;使用《全…