原文鏈接: https://mp.weixin.qq.com/s/vHTUBTlpMtNN0XvLQ_iyhg
Clean 架構(gòu)是 Uncle Bob 提出的一種軟件架構(gòu),Bob 大叔同時也是 SOLID 原則的命名者。Clean 架構(gòu)圖如下:

這張圖描述的是整個軟件系統(tǒng)的架構(gòu),而不是單體軟件,其中至少包括服務(wù)端以及客戶端。
對于 Android 單體應(yīng)用開發(fā)來說應(yīng)該還需要一個更貼切更精確的 Clean 架構(gòu)圖。
我大概總結(jié)了一下過往的開發(fā)經(jīng)驗,找出了應(yīng)用架構(gòu)中的重要部分,然后繪制了下面這張 Clean 架構(gòu)指導(dǎo)下的 Android 應(yīng)用架構(gòu)圖:

以及一般數(shù)據(jù)流向圖:

依賴關(guān)系
Clean 架構(gòu)基本準則是源碼級別的內(nèi)層不依賴外層,依賴關(guān)系永遠是單向的,外層向內(nèi)層依賴。
如上,Model 層是沒有任何依賴的,UseCase 可以依賴 Model 和 Repo 等,但絕不能依賴 ViewModel,UI 層依賴 ViewModel,但 ViewModel 絕不能依賴 UI 層。
為了達到這種源碼級別的依賴關(guān)系,我們必須借助一些工具來實現(xiàn)依賴注入,一般可以使用 Hilt 或者 Koin 這樣的框架來實現(xiàn)。
另外,依賴注入不應(yīng)該被濫用,不是所有的對象都適合用依賴注入,只有那些有明確層次關(guān)系的模塊,互相有著明確的依賴關(guān)系的才需要。對于一些工具類,顯然是沒必要注入的
Model(領(lǐng)域模型)
業(yè)務(wù)模型,或者叫領(lǐng)域模型,是根據(jù)軟件業(yè)務(wù)設(shè)計出來的具體模型,一般來說會是個 data class,其中不包含任何業(yè)務(wù)邏輯,只是個單純的模型對象。
由于是在整個架構(gòu)的最內(nèi)層,所以不依賴任何其他模塊,并且相對穩(wěn)定,設(shè)計的時候需要考慮這點。如果模型發(fā)生變化,那意味著整個上層的依賴方都可能發(fā)生變化,需要重新測試。
在命名和包結(jié)構(gòu)上,領(lǐng)域模型不需要帶 Entity 之類的后綴,直接命名為像 User 一樣即可,但考慮到這是在軟件的最內(nèi)層,可能會被所有模塊依賴到,所以要盡可能貼近其設(shè)計目標,并且不能太過寬泛。在包結(jié)構(gòu)上,需要被存放在 model 包下面。
Adapter(數(shù)據(jù)適配器)
數(shù)據(jù)適配器層主要用來做數(shù)據(jù)轉(zhuǎn)換,主要有兩個職責:
? 轉(zhuǎn)換網(wǎng)絡(luò)接口實體數(shù)據(jù)類和領(lǐng)域模型。
? 領(lǐng)域模型之間的互相轉(zhuǎn)換。
Adapter 層也比較純粹,只負責簡單的數(shù)據(jù)轉(zhuǎn)換,而且對外暴漏的函數(shù)都是冪等函數(shù)。
如果數(shù)據(jù)轉(zhuǎn)換過程中涉及到復(fù)雜的業(yè)務(wù)邏輯,可以考慮先用 UseCase 處理完成后再交給 Adapter。但因為 Adapter 層比 UseCase 層更靠內(nèi),所以 Adapter 不能依賴 UseCase。
習慣上,我們會以待轉(zhuǎn)換類為開頭,Adapter 結(jié)尾命名,例如我們要把 UserEntity 轉(zhuǎn)換為 User, 那么應(yīng)該這么寫:
class UserEntityAdapter{
fun toUser(entity: UserEntity): User {
//...
}
}
Repo
對于我們 Android 開發(fā)來說,Repo 層應(yīng)該是對網(wǎng)絡(luò)接口或本地磁盤的數(shù)據(jù)讀寫的封裝,對于 Repo 的使用者來說,不需要關(guān)注具體的實現(xiàn),且 Repo 中一般不具備復(fù)雜的業(yè)務(wù)邏輯,只能包含簡單的數(shù)據(jù)處理邏輯。
Repo 應(yīng)當隱藏具體的實現(xiàn)細節(jié),不僅包括獲取方式是網(wǎng)絡(luò)還是本地數(shù)據(jù),也應(yīng)該隱藏對應(yīng)的實體數(shù)據(jù)類,這意味著 Repo 層對外暴漏的函數(shù)的入?yún)⒑统鰠⒉荒馨涌诜祷氐膶嶓w類,也不應(yīng)該包含數(shù)據(jù)庫表實體類,只能包含領(lǐng)域模型或者基本類型。我們給 Room 設(shè)計的數(shù)據(jù)庫表的 data class 應(yīng)該限制在 Repo 內(nèi)部,我們給 Retrofit 設(shè)計的接口返回數(shù)據(jù) data class 也同樣應(yīng)該限制在 Repo 內(nèi)部。
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)
}
}
// 疑問: 為什么repo要依賴UserEntityAdapter呢?
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)
}
}
除了上面說的相應(yīng)數(shù)據(jù)的轉(zhuǎn)換之外,請求數(shù)據(jù)也需要在 Repo 層轉(zhuǎn)換,對于 Post 請求來說,可能會存在一個請求實體,這個實體數(shù)據(jù)類最好也不要對外暴漏,可以在 Repo 層的請求方法入?yún)⒛抢镒鲆恍┺D(zhuǎn)換,最好能讓入?yún)⒏唵斡押?。Repo 層還有一個作用就是負責把從接口或者數(shù)據(jù)庫中出來的不友好的數(shù)據(jù)模型轉(zhuǎn)換成友好的數(shù)據(jù)模型。
另外,現(xiàn)在由于有了 BFF 的存在,在某些比較簡單的業(yè)務(wù)場景下我們可以為了方便做一些妥協(xié),也就是接口的響應(yīng)數(shù)據(jù)實體類可以穿透 Repo 層,直接給到 ViewModel,甚至是 UiState 使用,但應(yīng)該明白這只是為了方便的妥協(xié),并不是最佳實踐,需要嚴格控制影響范圍。
UseCase(用例)
UseCase 一般是指特定應(yīng)用場景下的業(yè)務(wù)邏輯,用例引導(dǎo)了數(shù)據(jù)在模型之間的輸入輸出,并且指揮著業(yè)務(wù)實體利用其中的關(guān)鍵業(yè)務(wù)邏輯來實現(xiàn)用例的設(shè)計目標。
因此,一個 UseCase 往往只包含一段具體的業(yè)務(wù)邏輯,他的輸入是基本類型或者領(lǐng)域模型,輸出也是,并且是冪等函數(shù),也就是純函數(shù),所以 Google 建議我們每個 UseCase 只包含一個公開的函數(shù),類似于下面這種寫法:
class DoSomethingUseCase {
operator fun invoke(xxx: Foo): Bar {
// ...
}
}
通過利用 Kotlin 特性來使 UseCase 在使用的時候達到直接使用函數(shù)的體感。
但考慮到依賴以及管理問題,UseCase 最好還是不要直接使用函數(shù)來實現(xiàn),應(yīng)當按照上面的方式,定義一個類,然后再暴露一個通過操作符重載的函數(shù)。
在使用 UseCase 時可以這么用:
class LoginViewModel @Inject constructor(
private val doSomething: DoSomethingUseCase,
): ViewModel(){
fun onLoginClick(){
doSomething()
}
}
UseCase 的問題
UseCase 的粒度非常細,基本上每個 UseCase 就是一個函數(shù),在復(fù)雜的業(yè)務(wù)背景下將會存在非常多的 UseCase,隨著業(yè)務(wù)的增加,對他們的管理將難以為繼。
因此,UseCase 需要一個有效的手段來進行管理,首先,應(yīng)當按功能對他們的包名進行劃分。同一個業(yè)務(wù)的 UseCase 最好具備相同的包名。
其次,我們不能陷入所有業(yè)務(wù)都用 UseCase 的極端情況中,很多時候,我們可以將一些極度類似的功能組織在一個類中,其中提供多個公開的方法,這樣的寫法在以前很常見,比如各種 Manager, Helper, Resolver 等,他們能有效的減少 UseCase 數(shù)量,并且相對簡單。
UiState
UiState 是用來描述當前 UI 狀態(tài)的集合類,一般來說應(yīng)該是個 data class。
UiState 一定是不可變類,如果希望更改其中的某個值,應(yīng)當重新創(chuàng)建一個對象,直接通過 data class 提供的 copy 方法即可,例如:
data class LoginUiState(
val name:String,
val avatar: String,
val consentAgreed: Boolean
)
fun onAgreeChecked(){
uistate = uiState.copy(
consentAgreed = true,
)
}
UiState 中的數(shù)據(jù)應(yīng)當盡可能的方便給 UI 直接使用,因為 UiState 本身就是為了 UI 設(shè)計的,例如對于一個需要顯示的格式化后的時間,格式化的邏輯最好放在 ViewModel 或者更內(nèi)層,而不是直接給 UiState 一個時間戳,讓 UI 層去格式化。很多時候看起來簡單的邏輯也可能犯錯誤,UI 層沒有能力處理異常。
在 ViewModel 中如果需要更新 UiState,可以直接通過 update 方法。
_uiState.update {
it.copy(
name = "zhangke"
)
}
ViewModel
ViewModel 負責管理 UI 狀態(tài),執(zhí)行對應(yīng)的業(yè)務(wù)邏輯。因此 ViewModel 的生命周期與頁面是一致的。一般來說我們會通過直接使用 Jetpack 提供的 ViewModel,但也可以自己創(chuàng)建其他類型的 ViewModel,只要控制好生命周期即可。ViewModel 主要負責兩件事情:? 對外提供當前 UI 狀態(tài)? 接收 UI 事件并作出響應(yīng)當前 UI 狀態(tài)我們通過將 UiState 包裝在 StateFlow 里對外提供。
private val _uiState = MutableStateFlow()
val uiState:StateFlow<UserUiState> = _uiState.asStateFlow()
接受 UI 事件這點需要注意,ViewModel 需要做的是接收 UI 事件,例如用戶手勢輸入,至于用戶點擊之后要做什么事情這是 ViewModel 的內(nèi)部邏輯,不應(yīng)該對外暴露。
UI 層
我們這里說的 UI 層就是指一個頁面,除了常規(guī)的 Activity/Fragment 之外,對于 Compose 來說一個頁面可能對應(yīng)的是一個 Composable 函數(shù),這取決于 UI 層的實現(xiàn)。
UI 層應(yīng)該完全是數(shù)據(jù)驅(qū)動的,UI 層的作用就是百分之百的將 UiState 渲染出來,UiState 發(fā)生變化,UI 也跟著變化,這一點聲明式 UI 框架做的很好。
UI 層雖然也可以處理一些簡單的事件,但大部分的事件都還是要交給 ViewModel 來處理。
以上就是 Android 整潔架構(gòu)中的一些關(guān)鍵概念的介紹,我已經(jīng)按照這個架構(gòu)開發(fā)了一年多了,目前看下來確實會讓架構(gòu)很整潔,但對于一些復(fù)雜的業(yè)務(wù)場景,尤其是可能需要穿透多個層級,跨越常規(guī)生命周期的模塊就需要更精細的設(shè)計了。