Android Jetpack架構(gòu)中ViewModel接口暴露的不合理探究
在 Jetpack 架構(gòu)規(guī)范中, ViewModel 與 View 之間應(yīng)該遵循單向數(shù)據(jù)流的通信方式,Events 永遠(yuǎn)從 View 流向 VM ,而 State 從 VM 流向 View。

如果 ViewModel 對(duì) View 暴露了不適當(dāng)?shù)慕涌陬?lèi)型,則會(huì)破壞單向數(shù)據(jù)流的形成。不適當(dāng)?shù)慕涌陬?lèi)型常見(jiàn)于以下兩點(diǎn):
- 暴露 Mutable 狀態(tài)
- 暴露 Suspend 方法
暴露 Mutable 狀態(tài)
ViewModel 對(duì)外暴露的數(shù)據(jù)狀態(tài),無(wú)論是 LiveData 或是 StateFlow 都應(yīng)該使用 Immutable 的接口類(lèi)型進(jìn)行暴露而非 Mutable 的具體實(shí)現(xiàn)。View 只能單向訂閱這些狀態(tài)的變化,避免對(duì)狀態(tài)反向更新。
class MyViewModel: ViewModel() {
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean>
get() = _loading
}
未來(lái)避免暴露 Mutable 類(lèi)型,我們需要像上面這樣處理,將 loading 的具體實(shí)現(xiàn)定義為一個(gè) private 的 Mutable 類(lèi)型,便于內(nèi)部更新。
private val _loading : MutableStateFlow<Boolean?> = MutableStateFlow(null) val loading = _loading.asStateFlow()
StateFlow 的寫(xiě)法也類(lèi)似,但是通過(guò) asStateFlow 可以少寫(xiě)一個(gè)類(lèi)型聲明,但是要注意此時(shí)不要使用 custom get(), 不然 asStateFlow 會(huì)執(zhí)行多次。
每次都要多聲明一個(gè)帶劃線的私有變量會(huì)讓代碼顯得有些累贅,也正因如此,有 issue 希望 Kotlin 增加類(lèi)似下面的語(yǔ)法使得對(duì)外對(duì)內(nèi)可以暴露不同類(lèi)型。
//https://youtrack.jetbrains.com/issue/KT-14663
private val loading = MutableLiveData<Boolean>()
public get(): LiveData<Boolean>
在新語(yǔ)法還未出現(xiàn)的當(dāng)下,一個(gè)讓代碼變整潔的思路是為 ViewModel 提取對(duì)外暴露的抽象類(lèi):
abstract class MyViewModel: ViewModel() {
abstract val loading: LiveData<Boolean>
}
class MyViewModelImpl: MyViewModel() {
override val loading = MutableLiveData<Boolean>()
fun doSomeWork() {
// ...
loading.value = true
}
}如上, MyViewModelImpl 內(nèi)重寫(xiě)的 loading 可以作為 Mutable 類(lèi)型使用。雖然這種做法會(huì)增加了一個(gè)抽象類(lèi)代碼量不減反增,但是它使 MyViewModelImpl 內(nèi)的代碼更加簡(jiǎn)潔,而且對(duì)外可以隱藏更多 ViewModel 的實(shí)現(xiàn)細(xì)節(jié),封裝性更好。
但是需要特別注意的是,為了創(chuàng)建 MyViewModel 必須使用自定義 Factory:
val vm : MyViewModel by viewModels { MyViewModelFactory() }
如果你的工程引入了 Hilt ,那么可以通過(guò) @Bind 綁定 ViewModel 的接口與實(shí)現(xiàn),無(wú)需自定義 Factory 了,寫(xiě)法跟以前一樣,直接使用 by viewModels() 即可
@Module
@InstallIn(ViewModelComponent::class)
abstract class MyViewModule {
@Binds
abstract fun MyViewModel(instance: MyViewModelImpl): MyViewModel
}
@HiltViewModel
class MyViewModelImpl @Inject constructor() : MyViewModel()暴露 Suspend 方法
相對(duì)于暴露 Mutable 狀態(tài),暴露 Suspend 方法的錯(cuò)誤則更為常見(jiàn)。
按照單向數(shù)據(jù)流的思想 ViewModel 需要提供 API 給 View 用于發(fā)送 Events,我們?cè)诙x API 時(shí)需要注意避免使用 Suspend 函數(shù),理由如下:
- 來(lái)自 ViewModel 的數(shù)據(jù)應(yīng)該通過(guò)訂閱 UiState 獲取,因此 ViewModel 的其他方法方法不應(yīng)該有返回值,而 suspend 函數(shù)會(huì)鼓勵(lì)返回值的出現(xiàn)。
- 理想的 MVVM 中 View 的職責(zé)僅僅是渲染 UI,業(yè)務(wù)邏輯盡量移動(dòng)到 ViewModel 執(zhí)行,利于單元測(cè)試的同時(shí),
ViewModelScope可以保證一些耗時(shí)任務(wù)的穩(wěn)定執(zhí)行。如果暴露掛起函數(shù)給 View,則協(xié)程需要在lifecycleScope中啟動(dòng),在橫豎屏等場(chǎng)景中會(huì)中斷任務(wù)的進(jìn)行。
因此,ViewModel 為 View 暴露的 API 應(yīng)該是非掛起且無(wú)法返回值的方法,以下是官網(wǎng)的代碼實(shí)例:
// DO create coroutines in the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun loadNews() {
viewModelScope.launch {
val latestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
}
}
}
// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
// DO NOT do this. News would probably need to be refreshed as well.
// Instead of exposing a single value with a suspend function, news should
// be exposed using a stream of data as in the code snippet above.
suspend fun loadNews() = getLatestNewsWithAuthors()
}
代碼中建議暴露一個(gè)普通的無(wú)返回值的 loadNews ,而 latestNewsWithAuthors 的信息應(yīng)該通過(guò)訂閱 LatestNewsUiState 獲得 。
有一點(diǎn)讓人迷惑的是,官方文檔上有這么一句話:
Suspend functions in the ViewModel can be useful if instead of exposing state using a stream of data, only a single value needs to be emitted.
對(duì)于單發(fā)數(shù)據(jù)的請(qǐng)求允許使用掛起函數(shù)返回。但我建議大家忘掉這句話,理由有兩點(diǎn):
- 掛起函數(shù)的口子一開(kāi)就容易不分場(chǎng)景的濫用,如果整體數(shù)據(jù)流結(jié)構(gòu)造成破壞反而因小失大,索性應(yīng)該從源頭禁止
- 理論上來(lái)說(shuō),UI 上不存在單發(fā)數(shù)據(jù)請(qǐng)求的必要性,完全可以通過(guò)良好的設(shè)計(jì)轉(zhuǎn)化成 UiState ,這也更符合響應(yīng)式的編程模型。
到此這篇關(guān)于Android Jetpack架構(gòu)中ViewModel接口暴露的不合理探究的文章就介紹到這了,更多相關(guān)Android ViewModel接口內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開(kāi)發(fā)筆記之:消息循環(huán)與Looper的詳解
本篇文章是對(duì)Android中消息循環(huán)與Looper的應(yīng)用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
Android開(kāi)發(fā)之多媒體文件獲取工具類(lèi)實(shí)例【音頻,視頻,圖片等】
這篇文章主要介紹了Android開(kāi)發(fā)之多媒體文件獲取工具類(lèi),結(jié)合實(shí)例形式分析了Android獲取音頻,視頻及圖片等多媒體資源的相關(guān)操作技巧,需要的朋友可以參考下2017-10-10
Android應(yīng)用實(shí)現(xiàn)安裝后自啟動(dòng)的方法
今天小編就為大家分享一篇Android應(yīng)用實(shí)現(xiàn)安裝后自啟動(dòng)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
Android開(kāi)發(fā)之HTTP訪問(wèn)網(wǎng)絡(luò)
這篇文章主要介紹了Android開(kāi)發(fā)之HTTP訪問(wèn)網(wǎng)絡(luò)的相關(guān)資料,需要的朋友可以參考下2016-07-07
Android 啟動(dòng)另一個(gè)App/apk中的Activity實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 啟動(dòng)另一個(gè)App/apk中的Activity實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04
Android fragment 轉(zhuǎn)場(chǎng)動(dòng)畫(huà)創(chuàng)建步驟
在 Android 中,可以使用 setCustomAnimations() 方法來(lái)繪制自定義的 Fragment 轉(zhuǎn)場(chǎng)動(dòng)畫(huà),本文分步驟給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-03-03
Flutter上的數(shù)據(jù)監(jiān)控深入理解
這篇文章主要給大家介紹了關(guān)于Flutter上的數(shù)據(jù)監(jiān)控的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Flutter具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06

