Jetpack MVVM 常見錯誤三:錯誤的 ViewModel 數(shù)據(jù)加載時機

image.png

ViewModel 數(shù)據(jù)的首次加載時機?

在 MVVM 中, ViewModel 的重要職責是解耦 View 與 Model体谒。

  • View 向 ViewModel 發(fā)出指令填抬,請求數(shù)據(jù)
  • View 通過 DataBinding 或 LiveData 等訂閱 ViewModel 的數(shù)據(jù)變化
image.png

關于訂閱 ViewModel 的時機窘问,大家一般放在 onViewCreated 辆童,這是沒有問題的。但是一個常犯的錯誤是將 ViewModel 中首次的數(shù)據(jù)加載也放到 onViewCreated 中進行:

//DetailTaskViewModel.kt
class DetailTaskViewModel : ViewModel() {

    private val _task = MutableLiveData<Task>()
    val task: LiveData<Task> = _task

    fun fetchTaskData(taskId: Int) {
        viewModelScope.launch {
            _task.value = withContext(Dispatchers.IO){
                TaskRepository.getTask(taskId)
            }
        }
    }

}

//DetailTaskFragment.kt
class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){

    private val viewModel : DetailTaskViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //訂閱 ViewModel
        viewMode.uiState.observe(viewLifecycleOwner) {
           //update ui
        }

        //請求數(shù)據(jù)
        viewModel.fetchTaskData(requireArguments().getInt(TASK_ID))
    }
}

如上惠赫,如果 ViewModel 在 onViewCreated 中請求數(shù)據(jù)胸遇,當 View 因為橫豎屏等原因重建時會再次請求,而我們知道 ViewModel 的生命周期長于 View汉形,數(shù)據(jù)可以跨越 View 的生命周期存在,所以沒有必要隨著 View 的重建反復請求倍阐。

image.png

正確的加載時機

ViewModel 的初次數(shù)據(jù)加載推薦放到 init{} 中進行概疆,這樣可以保證 ViewModelScope 中只加載一次

//TasksViewModel.kt
class TasksViewModel: ViewModel() {

    private val _tasks = MutableLiveData<List<Task>>()
    val tasks: LiveData<List<Task>> = _uiState
    
    init {
        viewModelScope.launch {
            _tasks.value = withContext(Dispatchers.IO){
                TasksRepository.fetchTasks()
            }
        }
    }
}

LiveData KTX Builder

此外 lifecycle-livedata-ktx 提供的 LiveData KTX Builder 可以在創(chuàng)建 LiveData 的同時進行數(shù)據(jù)請求,無需創(chuàng)建 MutableLiveData峰搪,寫法更簡潔:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:$latest_version"

val tasks: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(repo.fetchData()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

Note: 此種 KTX Builder 只適用于數(shù)據(jù)僅加載一次的情況岔冀,如果后續(xù)有用戶動態(tài)觸發(fā)的數(shù)據(jù)請求,則還需要借助 MutableLiveData 來實現(xiàn)概耻。

設置 ViewModel 的初始化參數(shù)

如果在 ViewModel 構造函數(shù)中請求數(shù)據(jù)使套,當需要參數(shù)時該如何傳入呢罐呼? 比如我們最開頭例子中需要傳入一個 TaskId。

1. 構造參數(shù)

最容易想到的方法是通過構造參數(shù)傳入侦高。

class DetailTaskViewModel(private val taskId: Int) : ViewModel() {
 
    //...
    init {
        viewModelScope.launch {
            _tasks.value = TasksRepository.fetchTask(taskId)
        }
    }
}

需要注意不能直接調(diào)用 ViewModel 的構造函數(shù)構造嫉柴,這樣無法將 ViewModel 存入 ViewModelStore

此時需要定義一個 ViewModelProvider.Factory

class TaskViewModelFactory(val taskId: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
        modelClass.getConstructor(Int::class.java)
            .newInstance(taskId)
}

然后在 Fragment 中奉呛,用此 Factory 創(chuàng)建 ViewModel

class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){

    private val viewModel : DetailTaskViewModel by viewModels {
        TaskViewModelFactory(requireArguments().getInt(TASK_ID))
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //...
    }
}

2. 使用 SavedStateHandler

Fragment 1.2.0 或者 Activity 1.1.0 起, 可以使用 SavedStateHandle 作為 ViewModel 的參數(shù)计螺。 SavedStateHandle 可以幫助 ViewModel 實現(xiàn)數(shù)據(jù)持久化,同時可以傳遞 Fragment 的 arguments 給 ViewModel瞧壮。

關于如何使用 SavedStateHandle 對數(shù)據(jù)進行持久化登馒,由于不是本文重點不做介紹,這里只展示如何通過 SavedStateHandle 獲取 arguments

implementation "androidx.lifecycle:lifecycle-viewmodel-savestate:$latest_version"

SavedStateHandle 版本的 ViewModel 定義如下:

class TaskViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {

    //...
    init {
        viewModelScope.launch {
            _tasks.value = TasksRepository.fetchTask(
                savedStateHandle.get<Int>(TASK_ID)
            )
        }
    }
}

Fragment 中創(chuàng)建 ViewModel 如下:

class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){

    private val viewModel: TaskViewModel by viewModels {
        SavedStateViewModelFactory(
            requireActivity().application,
            requireActivity(),
            arguments// 將arguments作為默認參數(shù)傳遞給 SavedStateHandler
        )
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //...
    }
}

其中咆槽,SavedStateViewModelFactory 是關鍵陈轿,它會在構造 ViewModel 的時候,傳入 SavedStateHandler

3. 自定義擴展方法

前兩種方法的模板代碼較多秦忿,這里推薦一個自定義的擴展方法viewModelByFactory麦射,可以進一步簡化代碼


typealias CreateViewModel = (handle: SavedStateHandle) -> ViewModel

inline fun <reified VM : ViewModel> Fragment.viewModelByFactory(
    defaultArgs: Bundle? = null,
    noinline create: CreateViewModel = {
        val constructor = findMatchingConstructor(VM::class.java, arrayOf(SavedStateHandle::class.java))
        constructor!!.newInstance(it)
    }
): Lazy<VM> {
    return viewModels {
        createViewModelFactoryFactory(this, defaultArgs, create)
    }
}

inline fun <reified VM : ViewModel> Fragment.activityViewModelByFactory(
    defaultArgs: Bundle? = null,
    noinline create: CreateViewModel
): Lazy<VM> {
    return activityViewModels {
        createViewModelFactoryFactory(this, defaultArgs, create)
    }
}

fun createViewModelFactoryFactory(
    owner: SavedStateRegistryOwner,
    defaultArgs: Bundle?,
    create: CreateViewModel
): ViewModelProvider.Factory {
    return object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
        override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
            @Suppress("UNCHECKED_CAST")
            return create(handle) as? T
                ?: throw IllegalArgumentException("Unknown viewmodel class!")
        }
    }
}

@PublishedApi
internal fun <T> findMatchingConstructor(
    modelClass: Class<T>,
    signature: Array<Class<*>>
): Constructor<T>? {
    for (constructor in modelClass.constructors) {
        val parameterTypes = constructor.parameterTypes
        if (Arrays.equals(signature, parameterTypes)) {
            return constructor as Constructor<T>
        }
    }
    return null
}

使用時的效果如下:


class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){
    
    private val viewModel by viewModelByFactory(arguments)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //...
    }
}

除了 SavedStateHandler 以外如果還希望增加更多參數(shù),還可以自定義 CreateViewModel

4. 依賴注入

最后看一下如何使用依賴注入傳參小渊。以 Hilt 為例法褥,Hilt 天然支持 ViewModel 的依賴注入,本質(zhì)上也是基于 SavedStateHandler 實現(xiàn)的

@HiltViewModel
class DetailedTaskViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
  //...
}

添加 @HiltViewModel 注解酬屉,并使用 @Inject 注解構造函數(shù)半等。 除了 SavedStateHandle以外,也可以注入其他更多參數(shù)

ViewModel 的使用處呐萨, 別忘添加 @AndroidEntryPoint

@AndroidEntryPoint
class DetailedTaskFragment : Fragment(R.layout.fragment_detailed_task){

    private val viewModel : DetailedTaskViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //...
    }
}

前三種方式或多或少都要使用 ViewModelProvider.Factory 來構造 ViewModel, 而 Hilt 避免了 Factory 的使用杀饵,在寫法上最為簡單。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谬擦,一起剝皮案震驚了整個濱河市切距,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惨远,老刑警劉巖谜悟,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異北秽,居然都是意外死亡葡幸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門贺氓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔚叨,“玉大人,你說我怎么就攤上這事∶锼” “怎么了邢锯?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長搀别。 經(jīng)常有香客問我丹擎,道長,這世上最難降的妖魔是什么领曼? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任鸥鹉,我火速辦了婚禮,結果婚禮上庶骄,老公的妹妹穿的比我還像新娘毁渗。我一直安慰自己,他們只是感情好单刁,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布灸异。 她就那樣靜靜地躺著,像睡著了一般羔飞。 火紅的嫁衣襯著肌膚如雪肺樟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天逻淌,我揣著相機與錄音么伯,去河邊找鬼。 笑死卡儒,一個胖子當著我的面吹牛田柔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骨望,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼硬爆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了擎鸠?” 一聲冷哼從身側響起缀磕,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎劣光,沒想到半個月后袜蚕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡绢涡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年牲剃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垂寥。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滞项,到底是詐尸還是另有隱情狭归,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布文判,位于F島的核電站过椎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏戏仓。R本人自食惡果不足惜疚宇,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赏殃。 院中可真熱鬧敷待,春花似錦、人聲如沸仁热。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抗蠢。三九已至举哟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間迅矛,已是汗流浹背妨猩。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秽褒,地道東北人壶硅。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像震嫉,于是被迫代替她去往敵國和親森瘪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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