掌握 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)系作者
  • 序言:七十年代末氛改,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子比伏,更是在濱河造成了極大的恐慌胜卤,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赁项,死亡現(xiàn)場離奇詭異葛躏,居然都是意外死亡,警方通過查閱死者的電腦和手機悠菜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門舰攒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悔醋,你說我怎么就攤上這事摩窃。” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵猾愿,是天一觀的道長鹦聪。 經(jīng)常有香客問我,道長蒂秘,這世上最難降的妖魔是什么泽本? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮姻僧,結(jié)果婚禮上规丽,老公的妹妹穿的比我還像新娘。我一直安慰自己段化,他們只是感情好嘁捷,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布造成。 她就那樣靜靜地躺著显熏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晒屎。 梳的紋絲不亂的頭發(fā)上喘蟆,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音鼓鲁,去河邊找鬼蕴轨。 笑死,一個胖子當著我的面吹牛骇吭,可吹牛的內(nèi)容都是我干的橙弱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼燥狰,長吁一口氣:“原來是場噩夢啊……” “哼棘脐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起龙致,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蛀缝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后目代,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屈梁,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年榛了,在試婚紗的時候發(fā)現(xiàn)自己被綠了在讶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡霜大,死狀恐怖构哺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情僧诚,我是刑警寧澤遮婶,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布蝗碎,位于F島的核電站,受9級特大地震影響旗扑,放射性物質(zhì)發(fā)生泄漏蹦骑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一臀防、第九天 我趴在偏房一處隱蔽的房頂上張望眠菇。 院中可真熱鬧,春花似錦袱衷、人聲如沸捎废。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽登疗。三九已至,卻和暖如春嫌蚤,著一層夾襖步出監(jiān)牢的瞬間辐益,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工脱吱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留智政,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓箱蝠,卻偏偏與公主長得像续捂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宦搬,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容