詳解Android官方架構(gòu)中UseCase
1. UseCase 的用途
Android 最新的架構(gòu)規(guī)范中,引入了 Domain Layer(譯為領(lǐng)域?qū)觨r網(wǎng)域?qū)樱ㄗh大家使用 UseCase 來(lái)封裝一些復(fù)雜的業(yè)務(wù)邏輯。
傳統(tǒng)的 MVVM 架構(gòu)中,我們習(xí)慣用 ViewModel 來(lái)承載業(yè)務(wù)邏輯,隨著業(yè)務(wù)規(guī)模的擴(kuò)大,ViewModel 變得越來(lái)越肥大,職責(zé)不清。
Clean Architecture 提出的關(guān)注點(diǎn)分離和單一職責(zé)(SRP)的設(shè)計(jì)原則被廣泛認(rèn)可,因此 Android 在最新架構(gòu)中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 歸屬 UI Layer,更加聚焦 UiState 的管理,UI 無(wú)關(guān)的業(yè)務(wù)邏輯下沉 UseCase,UseCase 與 ViewModel 解耦后,也可以跨 ViewModel 提供公共邏輯。
Android 架構(gòu)早期的示例代碼 todo-app 中曾經(jīng)引入過(guò) UseCase 的概念,最新架構(gòu)中只不過(guò)是將 UseCase 的思想更明確了,最新的 UseCase 示例可以從官方的 NIA 中學(xué)習(xí)。
2. UseCase 的特點(diǎn)
官方文檔認(rèn)為 UseCase 應(yīng)該具有以下幾個(gè)特點(diǎn):
2.1 不持有狀態(tài)
可以定義自己的數(shù)據(jù)結(jié)構(gòu)類(lèi)型,但是不能持有狀態(tài)實(shí)例,像一個(gè)純函數(shù)一樣工作。甚至直接推薦大家將邏輯重寫(xiě)到 invoke 方法中,像調(diào)用函數(shù)一樣調(diào)用實(shí)例。
下面是 NIA 中的一個(gè)示例:GetRecentSearchQueriesUseCase
:
2.2 單一職責(zé)
嚴(yán)格遵守單一職責(zé),一個(gè) UseCase 只做一件事情,甚至其命名就是一個(gè)具體行為。掃一眼 UseCase 的文件目錄大概就知道 App 的大概功能了。
下面 NIA 中所有 UseCases:
2.3 可有可無(wú)
官方文檔中將 UseCase 定義為可選的角色,按需定義。簡(jiǎn)單的業(yè)務(wù)場(chǎng)景中允許 UI 直接訪問(wèn) Repository。如果我們將 UseCase 作為 UI 與 Data 隔離的角色,那么工程中會(huì)出現(xiàn)很多沒(méi)有太大價(jià)值的 UseCase ,可能就只有一行調(diào)用 Repoitory 的代碼。
3. 如何定義 UseCase
如上所述,官方文檔雖然對(duì) UseCase 給出了一些基本定義,但是畢竟是一個(gè)新新生概念,很多人在真正去寫(xiě)代碼的時(shí)候仍然會(huì)感覺(jué)不清晰,缺少有效指引。在究竟如何定義 UseCase 這個(gè)問(wèn)題上,還有待大家更廣泛的討論,形成可參考的共識(shí)。本文也是帶著這個(gè)目的而生,算是拋磚引玉吧。
3.1 Optional or Mandatory?
首先,官方文檔認(rèn)為 UseCase 是可選的,雖然其初衷是好的,大家都不希望出現(xiàn)太多 One-Liner 的 UseCase,但是作為一個(gè)架構(gòu)規(guī)范切忌模棱兩可,這種“可有可無(wú)”的規(guī)則其結(jié)局往往就是“無(wú)”。
業(yè)務(wù)剛起步時(shí)由于比較簡(jiǎn)單往往定義在 Repository 中,隨著業(yè)務(wù)規(guī)模的擴(kuò)大,應(yīng)該適當(dāng)?shù)迷黾?UseCase 封裝一些復(fù)雜的業(yè)務(wù)邏輯,但是實(shí)際項(xiàng)目中此時(shí)的重構(gòu)成本會(huì)讓開(kāi)發(fā)者變得“懶惰”,UseCase 最終難產(chǎn)。
那放棄 UseCase 呢?這可能會(huì)造成 Repository 的職責(zé)不清和無(wú)限膨脹,而且 Repository 往往不止有一個(gè)方法, ViewModel 直接依賴 Repository 也違反了 SOLID 中的另一個(gè)重要原則 ISP ,ViewModel 會(huì)因?yàn)椴幌嚓P(guān)的 Repository 改動(dòng)導(dǎo)致重新編譯。
ISP(Interface Segregation Principle,接口隔離原則) 要求將接口分離成更小的和更具體的接口,以便調(diào)用方只需知道其需要使用的方法。這可以提高代碼的靈活性和可重用性,并減少代碼的依賴性和耦合性。
為了降低前期判斷成本和后續(xù)重構(gòu)成本,如果我們有業(yè)務(wù)持續(xù)壯大的預(yù)期,那不妨考慮將 UseCase 作為強(qiáng)制選項(xiàng)。當(dāng)然,最好這需要研究如何降低 UseCase 帶來(lái)的模板代碼。
3.2 Class or Object?
官方建議使用 Class
定義 UseCase,每次使用都實(shí)例化一個(gè)新對(duì)象,這會(huì)做成一些重復(fù)開(kāi)銷(xiāo),那么可否用 object
定義 UseCase 呢?
UseCase 理論上可以作為單例存在,但 Class 相對(duì)于 Object 有以下兩個(gè)優(yōu)勢(shì):
- UseCase 希望像純函數(shù)一樣工作,普通 Class 可以確保每次使用時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例,從而避免狀態(tài)共享和副作用等問(wèn)題。
- 普通類(lèi)可以通過(guò)構(gòu)造參數(shù)注入不同的 Repository,UseCase 更利于復(fù)用和單元測(cè)試
如果我們強(qiáng)烈希望 UseCase 有更長(zhǎng)的生命周期,那借助 DI 框架,普通類(lèi)也可以簡(jiǎn)單的支持。例如 Dagger 中只要添加 @Singleton
注解即可
@Singleton class GetRecentSearchQueriesUseCase @Inject constructor( private val recentSearchRepository: RecentSearchRepository, ) { operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> = recentSearchRepository.getRecentSearchQueries(limit) }
3.3 Class or Function?
既然我們想像函數(shù)一樣使用 UseCase ,那為什么不直接定義成 Function
呢?比如像下面這樣
fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>>
這確實(shí)遵循了 FP 的原則,但又喪失了 OOP 封裝性的優(yōu)勢(shì):
- UseCase 往往需要依賴 Repository 對(duì)象,一個(gè) UseCase Class 可以將 Repository 封裝為成員存儲(chǔ)。而一個(gè) UseCase Function 則需要調(diào)用方通過(guò)參數(shù)傳入,使用成本高不說(shuō),如果 UseCase 依賴的 Repository 的類(lèi)型或者數(shù)量發(fā)生變化了,調(diào)用方需要跟著修改
- 函數(shù)起不到隔離 UI 和 Data 的作用,ViewModel 仍然需要直接依賴 Repository,為 UseCase 傳參
- UseCase Class 可以定義一些 private 的方法,相對(duì)于 Function 更能勝任一些復(fù)雜邏輯的實(shí)現(xiàn)
可見(jiàn),在 UseCase 的定義上 Function 沒(méi)法取代 Class。當(dāng)然 Class 也帶來(lái)一些弊端:
- 暴露多個(gè)方法,破壞 SRP 原則。所以官方推薦用
verb in present tense + noun/what (optional) + UseCase
動(dòng)詞命名,也是想讓職責(zé)更清晰。 - 攜帶可變狀態(tài),這是大家寫(xiě) OOP 的慣性思維
- 樣板代碼多
3.4 Function interface ?
通過(guò)前面的分析我們知道:UseCase 的定義需要兼具 FP 和 OOP 的優(yōu)勢(shì)。這讓我想到了 Function(SAM) Interface 。Function Interface 是一個(gè)單方法的接口,可以低成本創(chuàng)建一個(gè)匿名類(lèi)對(duì)象,確保對(duì)象只能有一個(gè)方法,同時(shí)具有一定封裝性,可以通過(guò)“閉包”依賴 Repository。此外,Kotlin 對(duì) SAM 提供了簡(jiǎn)化寫(xiě)法,一定程度也減少了樣板代碼。
Functional (SAM) interfaces: kotlinlang.org/docs/fun-in…
改用 Function interface 定義 GetRecentSearchQueriesUseCase 的代碼如下:
fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>
用它創(chuàng)建 UseCase 實(shí)例的同時(shí),實(shí)現(xiàn)函數(shù)中的邏輯
val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase { //... }
我在函數(shù)實(shí)現(xiàn)中如何 Repository 呢?這要靠 DI 容器獲取。官方示例代碼中都使用 Hilt 來(lái)解耦 ViewModel 與 UseCase 的,ViewModel 不關(guān)心 UseCase 的創(chuàng)建細(xì)節(jié)。下面是 NIA 的代碼, GetRecentSearchQueriesUseCase 被自動(dòng)注入到 SearchViewModel
中。
@HiltViewModel class SearchViewModel @Inject constructor( recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM //... ) : ViewModel() { //... }
Function interface 的 GetRecentSearchQueriesUseCase 沒(méi)有構(gòu)造函數(shù),需要通過(guò) Dagger 的 @Module
安裝到 DI 容器中,provideGetRecentSearchQueriesUseCase
參數(shù)中的 RecentSearchRepository 可以從容器中自動(dòng)獲取使用。
@Module @InstallIn(ActivityComponent::class) object UseCaseModule { @Provides fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) = GetRecentSearchQueriesUseCase { limit -> recentSearchRepository.getRecentSearchQueries(limit) } }
當(dāng)時(shí)用 Koin 作為 DI 容器時(shí)也沒(méi)問(wèn)題,代碼如下:
single<GetRecentSearchQueriesUseCase> { GetRecentSearchQueriesUseCase { limit -> recentSearchRepository.getRecentSearchQueries(limit) } }
4. 總結(jié)
UseCase 作為官方架構(gòu)中的新概念,尚沒(méi)有完全深入人心,需要不斷探索合理的使用方式,本文給出一些基本思考:
- 考慮到架構(gòu)的擴(kuò)展性,推薦在 ViewModel 與 Repository 之間強(qiáng)制引入 UseCase,即使眼下的業(yè)務(wù)邏輯并不復(fù)雜
- UseCase 不持有可變狀態(tài)但依賴 Repository,需要兼具 FP 與 OOP 的特性,更適合用 Class 定義而非 Function
- 在引入 UseCase 之前應(yīng)該先引入 DI 框架,確保 ViewModel 與 UseCase 的耦合。
- Function Interface 是 Class 之外的另一種定義 UseCase 的方式,有利于代碼更加函數(shù)式
以上就是詳解Android官方架構(gòu)中UseCase的詳細(xì)內(nèi)容,更多關(guān)于Android官方架構(gòu)UseCase 的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android Application級(jí)別自定義Toast
這篇文章主要為大家詳細(xì)介紹了Android Application級(jí)別自定義Toast,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Android開(kāi)發(fā)中ProgressDialog簡(jiǎn)單用法示例
這篇文章主要介紹了Android開(kāi)發(fā)中ProgressDialog簡(jiǎn)單用法,結(jié)合實(shí)例形式分析了Android使用ProgressDialog的進(jìn)度條顯示與關(guān)閉、更新等事件響應(yīng)相關(guān)操作技巧,需要的朋友可以參考下2017-10-10Android實(shí)現(xiàn)頁(yè)面短信驗(yàn)證功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)頁(yè)面短信驗(yàn)證功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05Android開(kāi)發(fā)實(shí)例之多點(diǎn)觸控程序
本文主要介紹 Android開(kāi)發(fā)多點(diǎn)觸控,這里提供了詳細(xì)的資料和示例代碼,以及實(shí)現(xiàn)效果圖,有開(kāi)發(fā)Android應(yīng)用需要這樣的功能的小伙伴可以參考下2016-08-08Android編程實(shí)現(xiàn)google消息通知功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)google消息通知功能,結(jié)合具體實(shí)例形式分析了Android消息處理及C#服務(wù)器端與google交互的相關(guān)操作技巧,需要的朋友可以參考下2017-06-06Android在多種設(shè)計(jì)下實(shí)現(xiàn)懶加載機(jī)制的方法
這篇文章主要介紹了Android在多種設(shè)計(jì)下實(shí)現(xiàn)懶加載機(jī)制的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化
這篇文章主要為大家介紹了Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android PopupWindow被輸入法彈上去之后無(wú)法恢復(fù)原位的解決辦法
這篇文章主要介紹了Android PopupWindow被輸入法彈上去之后無(wú)法恢復(fù)原位的解決辦法,需要的朋友可以參考下2016-12-12Android中ScrollView監(jiān)聽(tīng)滑動(dòng)距離案例講解
這篇文章主要介紹了Android中ScrollView監(jiān)聽(tīng)滑動(dòng)距離案例講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08