如果您正在使用 ViewModels,請記住以下幾點以提高代碼質(zhì)量
在本系列文章中,我們將深入探討使用 Android ViewModels 的最佳實踐歼冰,強調(diào)提高代碼質(zhì)量的基本注意事項。我們將介紹 ViewModels 在管理 UI 狀態(tài)和業(yè)務邏輯方面的作用、懶惰依賴注入策略以及反應式編程的重要性粤咪。此外,我們還將討論應避免的常見陷阱渴杆,如不正確的狀態(tài)初始化和暴露可變狀態(tài)射窒,為開發(fā)人員提供全面的指南。
了解 VsiewModel
根據(jù) Android 文檔将塑,ViewModel 類充當業(yè)務邏輯或屏幕級狀態(tài)的持有者脉顿。它將狀態(tài)暴露給用戶界面,并封裝相關的業(yè)務邏輯点寥。它的主要優(yōu)點是緩存狀態(tài)艾疟,并通過配置更改將其持久化。這意味著用戶界面在活動間導航或配置更改(如旋轉(zhuǎn)屏幕)后無需再次獲取數(shù)據(jù)敢辩。
本系列討論要點
1.避免在 init {} 塊中初始化狀態(tài)蔽莱。
2.避免暴露可變狀態(tài)。
3.使用 MutableStateFlows 時使用 update{}:
4.懶得在構(gòu)造函數(shù)中注入依賴關系戚长。
5.多采用反應式編碼盗冷,少采用命令式編碼。
6.避免從外部初始化 ViewModel同廉。
7.避免從外部傳遞參數(shù)仪糖。
8.避免對Coroutine Dispatchers進行硬編碼柑司。
9.對 ViewModel 進行單元測試。
10.避免暴露懸浮函數(shù)
11.充分利用 ViewModels 中的 onCleared() 回調(diào)锅劝。
12.處理進程死亡和配置更改攒驰。
13.注入調(diào)用存儲庫的用例,存儲庫再調(diào)用數(shù)據(jù)源故爵。
14.在 ViewModels 中只包含域?qū)ο蟆?/p>
15.利用 shareIn() 和 stateIn() 操作符玻粪,避免多次沖擊上游。
這一章我們討論第1點
1-避免在 init {} 塊中初始化狀態(tài):
在 Android ViewModel 的 init {} 塊中啟動數(shù)據(jù)加載似乎很方便诬垂,可以在 ViewModel 創(chuàng)建后立即初始化數(shù)據(jù)劲室。但是,這種方法有幾個缺點结窘,如與 ViewModel 創(chuàng)建緊密耦合痹籍、測試難題、靈活性有限晦鞋、處理配置更改蹲缠、資源管理和 UI 響應速度。為了減少這些問題悠垛,建議使用更謹慎的數(shù)據(jù)加載方法线定,利用 LiveData 或其他生命周期感知組件,以尊重 Android 生命周期的方式管理數(shù)據(jù)确买。
與創(chuàng)建 ViewModel 緊密耦合:
在 init{} 塊中加載數(shù)據(jù)會將數(shù)據(jù)獲取與 ViewModel 的生命周期緊密聯(lián)系在一起斤讥。這可能會導致難以控制數(shù)據(jù)加載的時間,尤其是在復雜的用戶界面中湾趾,您可能希望對根據(jù)用戶交互或其他事件獲取數(shù)據(jù)的時間進行更精細的控制芭商。
對測試來講,增加了挑戰(zhàn):
測試變得更加困難搀缠,因為一旦 ViewModel 實例化铛楣,數(shù)據(jù)加載就會開始。這樣就很難在不觸發(fā)網(wǎng)絡請求或數(shù)據(jù)庫查詢的情況下孤立地測試 ViewModel艺普,從而使測試設置變得復雜簸州,并可能導致測試不穩(wěn)定。
靈活性有限:
在 ViewModel 實例化時自動開始數(shù)據(jù)加載會限制您處理不同用戶流或 UI 狀態(tài)的靈活性歧譬。例如岸浑,您可能希望延遲獲取數(shù)據(jù),直到授予某些用戶權(quán)限或用戶導航到應用程序的特定部分瑰步。
處理配置更改:
Android ViewModels 設計用于在配置發(fā)生變化(如屏幕旋轉(zhuǎn))后繼續(xù)運行矢洲。如果數(shù)據(jù)加載是在 init{} 塊中啟動的,那么如果不小心管理缩焦,配置更改可能會導致意外行為或不必要的數(shù)據(jù)重新獲取读虏。
資源管理:
即時數(shù)據(jù)加載可能會導致資源使用效率低下责静,尤其是當用戶進入應用程序或屏幕后并不需要立即使用數(shù)據(jù)時。對于需要消耗大量數(shù)據(jù)或使用高成本操作來獲取或處理這些數(shù)據(jù)的應用程序來說掘譬,這可能尤其成問題泰演。
用戶界面響應速度:
在 init{} 塊中啟動數(shù)據(jù)加載會影響 UI 響應速度呻拌,尤其是在數(shù)據(jù)加載操作時間較長或阻塞主線程的情況下葱轩。一般來說,好的做法是保持 init{} 塊的輕量級藐握,將繁重或異步操作卸載到后臺線程靴拱,或使用 LiveData/Flow 來觀察數(shù)據(jù)變化。
為了減少這些問題猾普,通常建議使用更謹慎的方法進行數(shù)據(jù)加載袜炕,例如根據(jù)特定的用戶操作或 UI 事件觸發(fā)數(shù)據(jù)加載,并利用 LiveData 或其他生命周期感知組件以尊重 Android 生命周期的方式管理數(shù)據(jù)初家。這有助于確保您的應用程序保持響應速度偎窘,更易于測試,并更有效地利用資源溜在。
讓我們來看看這種反模式的一些例子:
示例 #1:
class SearchViewModel @Inject constructor(
private val searchUseCase: dagger.Lazy<SearchUseCase>,
private val wordsUseCase: GetWordsUseCase,
) : ViewModel() {
data class UiState(
val isLoading: Boolean,
val words: List<String> = emptyList()
)
init {
getWords()
}
val _state = MutableStateFlow(UiState(isLoading = true))
val state: StateFlow<UiState>
get() = _state.asStateFlow()
private fun getWords() {
viewModelScope.launch {
_state.update { UiState(isLoading = true) }
val words = wordsUseCase.invoke()
_state.update { UiState(isLoading = false, words = words) }
}
}
}
在這個 SearchViewModel 中陌知,數(shù)據(jù)加載是在 init 代碼塊中立即觸發(fā)的,這使得數(shù)據(jù)獲取與 ViewModel 實例化緊密耦合掖肋,降低了靈活性仆葡。在類內(nèi)部暴露可變狀態(tài) _state,而不處理潛在的錯誤或不同的 UI 狀態(tài)(加載志笼、成功沿盅、錯誤),會導致實現(xiàn)不夠健壯且難以測試纫溃。這種方法削弱了 ViewModel 生命周期意識的優(yōu)勢和懶初始化的效率腰涧。
怎么樣處理更好呢?
改進 #1:
class SearchViewModel @Inject constructor(
private val searchUseCase: dagger.Lazy<SearchUseCase>,
private val wordsUseCase: GetWordsUseCase,
) : ViewModel() {
data class UiState(
val isLoading: Boolean = true,
val words: List<String> = emptyList()
)
val state: StateFlow<UiState> = flow {
emit(UiState(isLoading = true))
val words = wordsUseCase.invoke()
emit(UiState(isLoading = false, words = words))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState())
}
這次重構(gòu)刪除了 ViewModel initblock 中的數(shù)據(jù)獲取紊浩,轉(zhuǎn)而依賴集合來啟動數(shù)據(jù)加載南窗。這一改動大大提高了管理數(shù)據(jù)獲取的靈活性,減少了 ViewModel 實例化時不必要的操作郎楼,直接解決了過早加載數(shù)據(jù)的問題万伤,提高了 ViewModel 的響應速度和效率。
示例 #2:
class SearchViewModel @Inject constructor(
private val searchUseCase: SearchUseCase,
@IoDispatcher val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
private val searchQuery = MutableStateFlow("")
private val _uiState = MutableLiveData<SearchUiState>()
val uiState = _uiState
init {
viewModelScope.launch {
searchQuery.debounce(DEBOUNCE_TIME_IN_MILLIS)
.collectLatest { query ->
Timber.d("collectLatest(), query:[%s]", query)
if (query.isEmpty()) {
_uiState.value = SearchUiState.Idle
return@collectLatest
}
try {
_uiState.value = SearchUiState.Loading
val photos = withContext(ioDispatcher){
searchUseCase.invoke(query)
}
if (photos.isEmpty()) {
_uiState.value = SearchUiState.EmptyResult
} else {
_uiState.value = SearchUiState.Success(photos)
}
} catch (e: Exception) {
_uiState.value = SearchUiState.Error(e)
}
}
}
}
fun onQueryChanged(query: String?) {
query ?: return
searchQuery.value = query
}
sealed class SearchUiState {
object Loading : SearchUiState()
object Idle : SearchUiState()
data class Success(val photos: List<FlickrPhoto>) : SearchUiState()
object EmptyResult : SearchUiState()
data class Error(val exception: Throwable) : SearchUiState()
}
companion object {
private const val DEBOUNCE_TIME_IN_MILLIS = 300L
}
}
在 SearchViewModel 的 init 塊中啟動一個 coroutine 來立即處理數(shù)據(jù)呜袁,會將數(shù)據(jù)獲取與 ViewModel 的生命周期聯(lián)系得過于緊密敌买,從而可能導致效率低下和生命周期管理問題。這種方法有可能導致不必要的網(wǎng)絡調(diào)用阶界,并使錯誤處理復雜化虹钮,尤其是在用戶界面準備好處理或顯示此類信息之前聋庵。此外,這種方法假定 UI 更新會隱式返回主線程芙粱,但這并不總是安全或高效的祭玉,而且這種方法會在 ViewModel 實例化后立即啟動數(shù)據(jù)獲取,從而使測試更具挑戰(zhàn)性春畔。
我們可以將其重構(gòu)如下:
class SearchViewModel @Inject constructor(
private val searchUseCase: dagger.Lazy<SearchUseCase>,
) : ViewModel() {
private val searchQuery = MutableStateFlow("")
val uiState: LiveData<SearchUiState> = searchQuery
.debounce(DEBOUNCE_TIME_IN_MILLIS)
.asLiveData()
.switchMap(::createUiState)
private fun createUiState(query: @JvmSuppressWildcards String) = liveData {
Timber.d("collectLatest(), query:[%s]", query)
if (query.isEmpty()) {
emit(SearchUiState.Idle)
return@liveData
}
try {
emit(SearchUiState.Loading)
val photos = searchUseCase.get().invoke(query)
if (photos.isEmpty()) {
emit(SearchUiState.EmptyResult)
} else {
emit(SearchUiState.Success(photos))
}
} catch (e: Exception) {
emit(SearchUiState.Error(e))
}
}
fun onQueryChanged(query: String?) {
query ?: return
searchQuery.value = query
}
sealed class SearchUiState {
data object Loading : SearchUiState()
data object Idle : SearchUiState()
data class Success(val photos: List<FlickrPhoto>) : SearchUiState()
data object EmptyResult : SearchUiState()
data class Error(val exception: Throwable) : SearchUiState()
}
companion object {
private const val DEBOUNCE_TIME_IN_MILLIS = 300L
}
}
改進后的實現(xiàn)避免了在 init 代碼塊中直接啟動一個 coroutine 來觀察 searchQuery 的變化脱货,而是選擇了一種反應式設置,在 coroutine 上下文之外將 searchQuery 轉(zhuǎn)換為 LiveData律姨。這消除了與生命周期管理和 coroutine 取消相關的潛在問題振峻,確保數(shù)據(jù)獲取本質(zhì)上是生命周期感知的,而且更節(jié)省資源择份。由于不依賴 init 塊來開始觀察和處理用戶輸入扣孟,它還將 ViewModel 的初始化與其數(shù)據(jù)獲取邏輯分離開來,從而實現(xiàn)了更簡潔的關注點分離和更易于維護的代碼結(jié)構(gòu)荣赶。
總結(jié)
我們深入探討了在 init{} 塊中啟動數(shù)據(jù)加載會阻礙我們前進的原因凤价,并探索了通過 ViewModels 協(xié)調(diào)應用程序的 UI 和邏輯的更智能、更精簡的方法拔创。在整個過程中利诺,我們討論了直接的解決方案和基本策略,以避免經(jīng)常出現(xiàn)的陷阱伏蚊。
此文章為翻譯立轧,如有侵權(quán)請聯(lián)系我及時刪除,謝謝躏吊。
原文地址:[Mastering Android ViewModels: Essential Dos and Don’ts Part 1 ??? | by Reza | ProAndroidDev
]
編輯標簽