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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JVM类加载全过程

Java虚拟机类加载的全过程&#xff0c;即加载&#xff0c;验证&#xff0c;准备&#xff0c;解析&#xff0c;初始化 一、加载 加载 是 类加载过程中的一个阶段&#xff0c; 有以下三部分组成 1&#xff09;通过一个类的全限定名来获取定义此类的二进制流 2&#xff09;将这…

【Java 基础】19 多线程基础

文章目录 进程和线程进程&#xff08;Process&#xff09;线程&#xff08;Thread&#xff09; 线程的创建1&#xff09;继承 Thread 类2&#xff09;实现 Runnable 接口3&#xff09;使用 Lambda 表达式4&#xff09;总结 线程的状态状态的分类状态间转换 多线程是一种 同时执…

Github无法打开

文章目录 一、问题二、解决2.1、科学上网&#xff08;使用中&#xff09;2.2、使用代理&#xff08;不稳定&#xff09;2.3、修改hosts&#xff08;得更新&#xff09;2.3.1、找到hosts文件2.3.2、复制hosts文件2.3.3、添加记录2.3.4、替换原来的hosts文件2.3.5、成功访问Githu…

CefSharp 获取POST(AJAX)、GET消息返回值(request)

CefSharp作为专门为爬虫工具开发的库比Selenium这种开发目的是页面测试工具然后用来做爬虫的工具要贴心得多。我们操作网页的时候发送或者做了某个动作提交表单之后需要知道我们的动作或者提交是否成功&#xff0c;因为有的页面会因为网络延迟问题提交失败&#xff0c;需要准确…

[Azure]azure磁盘加密(Windows/Linux) ADE(Azure Disk Encryption)

Azure 磁盘加密用于保护数据&#xff0c;对于Windows使用BitLocker对磁盘进行加密&#xff0c;同时与Key Vault集成&#xff0c;控制和管理Key和Secret。 本文利用Potal对磁盘进行加密 注&#xff1a;Azure DIsk Encryption 可能会导致VM重启&#xff0c;对VM造成影响&#xff…

哈希与哈希表

哈希表的概念 哈希表又名散列表&#xff0c;官话一点讲就是&#xff1a; 散列表&#xff08;Hash table&#xff0c;也叫哈希表&#xff09;&#xff0c;是根据关键码值(Key value)而直接进行访问的数据结构。也就是说&#xff0c;它通过把关键码值映射到表中一个位置来访问记…

基于SSM的老年公寓信息管理的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

nodejs微信小程序+python+PHP天天网站书城管理系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

QT 中 QMessageBox 的简单用法

效果 思路 // 创建一个question弹出对话框&#xff0c;添加两个按钮&#xff1a;Yes和NoQMessageBox *box new QMessageBox(QMessageBox::Question, "提示", "确认删除的信息吗&#xff1f;", QMessageBox::Yes | QMessageBox::No, this);box->button(…

成人学生钢笔练字快速入门,硬笔书法行书楷书教程合集

一、教程描述 虽然现在都是电脑打字&#xff0c;需要手写的场合越来越少&#xff0c;但是可以写一手人见人爱&#xff0c;花见花开的好字&#xff0c;仍然是很拉风很惊艳的&#xff0c;可以给人留下深刻印象。本套硬笔书法教程&#xff0c;大小40.90G&#xff0c;共有591个文件…

Python 网络爬虫(二):HTTP 基础知识

《Python入门核心技术》专栏总目录・点这里 文章目录 1. HTTP 协议简述2. HTTP 请求过程3. HTTP 的结构3.1 请求行3.2 请求头3.3 请求体3.4 状态行3.5 响应头3.6 响应体 4. Cookie 状态管理5. HTTP 请求示例6. 总结 大家好&#xff0c;我是水滴~~ 在准备学习网络爬虫之前&…

公有云迁移研究——AWS Route53

大纲 1 什么是Route 532 Route 53能做些什么# 3 通过DNS托管来实现分流3.1 创建DNS托管3.2 对托管创建记录对流量进行分配 4 通过流量策略来对流量进行分流4.1 创建流量策略 5 对比两者的区别6 推荐 在给客户从本地机房往AWS迁移的过程中&#xff0c;我们接到如下需求&#xff…

vue打印功能

安装 vue3-print-nb yarn add vue3-print-nb //或 npm install vue3-print-nb main.js中引入 vue3-print-nb import { createApp } from vue; import App from ./App.vue; const app createApp(App); // 打印插件 import print from vue3-print-nb app.use(print) // 页面…

公有云迁移研究——AWS Translate

大纲 1 什么是Translate2 Aws Translate是怎么运作的3 Aws Translate和Google Translate的区别4 迁移任务4.1 迁移原因 5 Aws Translate的Go demo6 迁移中遇到的问题6.1 账号和权限问题&#xff1a;6.2 小语种 1 什么是Translate Translate是一种文本翻译服务&#xff0c;它使…

制作一个RISC-V的操作系统三-编译与链接

文章目录 GCCGCC简介GCC的命令格式gcc -Egcc -cgcc -Sgcc -ggcc -vGCC的主要执行步骤GCC涉及的文件类型针对多个源文件的处理 ELFELF介绍ELF文件格式ELF文件处理相关工具&#xff1a;Binutils&#xff08;binary utility&#xff09;readlelf -hreadelf -S或readelf -SW&#x…

思维模型 路径依赖定律

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。难以摆脱的惯性。 1 路径依赖定律的应用 1.1 打破路径依赖定律的苹果 在 20 世纪 80 年代&#xff0c;苹果公司推出了 Macintosh 电脑&#xff0c;这是一款具有图形用户界面和鼠标的创新产…

MacOS M芯片 安装MySQL5.7教程

目录 1. 安装Homebrew1.1 快速安装1.2 检查是否安装成功 2. 通过Homebrew安装MySQL2.1 搜索 MySQL 版本2.2 安装MySQL 5.72.3 位置说明2.4 启动MySQL服务2.5 检查服务状态2.6 设置环境变量2.7 重置密码 3. 测试安装 1. 安装Homebrew 1.1 快速安装 /bin/bash -c "$(curl …

SQL Server 数据库,多表查询

4.2使用T-SQL实现多表查询 前面讲述过的所有查询都是基于单个数据库表的查询&#xff0c;如果一个查询需要对多个表进行操作&#xff0c; 就称为联接查询&#xff0c;联接查询的结果集或结果称为表之间的联接。 联接查询实际上是通过各个表之间共同列的关联性来查询数据的&…

人工智能|网络爬虫——用Python爬取电影数据并可视化分析

一、获取数据 1.技术工具 IDE编辑器&#xff1a;vscode 发送请求&#xff1a;requests 解析工具&#xff1a;xpath def Get_Detail(Details_Url):Detail_Url Base_Url Details_UrlOne_Detail requests.get(urlDetail_Url, headersHeaders)One_Detail_Html One_Detail.cont…

后端部署-阿里云服务器-开设端口-域名解析-安全证书-备案

本文以阿里云的轻量级数据库为例子。 前言 要搭建一个完整的后端系统一般的步骤&#xff1a; 获得一台服务器----->开设端口----->搭建后台所需要的语言和应用---->利用公网ip地址测试后端程序------->购买域名和证书-------->域名绑定和解析------->icp备…