网站运营和推广,东莞万江网站制作,网站建设无形资产的账务处理,公司管理系统叫什么Clean 架构下的现代 Android 架构指南
Clean 架构是 Uncle Bob 提出的一种软件架构#xff0c;Bob 大叔同时也是 SOLID 原则的命名者。
Clean 架构图如下#xff1a; 这张图描述的是整个软件系统的架构#xff0c;而不是单体软件#xff0c;其中至少包括服务端以及客户端…Clean 架构下的现代 Android 架构指南
Clean 架构是 Uncle Bob 提出的一种软件架构Bob 大叔同时也是 SOLID 原则的命名者。
Clean 架构图如下 这张图描述的是整个软件系统的架构而不是单体软件其中至少包括服务端以及客户端。
对于 Android 单体应用开发来说应该还需要一个更贴切更精确的 Clean 架构图。
我大概总结了一下过往的开发经验找出了应用架构中的重要部分然后绘制了下面这张 Clean 架构指导下的 Android 应用架构图 以及一般数据流向图
依赖关系
Clean 架构基本准则是源码级别的内层不依赖外层依赖关系永远是单向的外层向内层依赖。
如上Model 层是没有任何依赖的UseCase 可以依赖 Model 和 Repo 等但绝不能依赖 ViewModelUI 层依赖 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:StateFlowUserUiState _uiState.asStateFlow()接受 UI 事件这点需要注意ViewModel 需要做的是接收 UI 事件例如用户手势输入至于用户点击之后要做什么事情这是 ViewModel 的内部逻辑不应该对外暴露。
UI 层
我们这里说的 UI 层就是指一个页面除了常规的 Activity/Fragment 之外对于 Compose 来说一个页面可能对应的是一个 Composable 函数这取决于 UI 层的实现。
UI 层应该完全是数据驱动的UI 层的作用就是百分之百的将 UiState 渲染出来UiState 发生变化UI 也跟着变化这一点声明式 UI 框架做的很好。
UI 层虽然也可以处理一些简单的事件但大部分的事件都还是要交给 ViewModel 来处理。
以上就是 Android 整洁架构中的一些关键概念的介绍我已经按照这个架构开发了一年多了目前看下来确实会让架构很整洁但对于一些复杂的业务场景尤其是可能需要穿透多个层级跨越常规生命周期的模块就需要更精细的设计了。