掌握 Android ViewModels: 必要的 "應做 "和 "不應做" 第 1 部分

如果您正在使用 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
]
編輯標簽

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容