Jetpack MVVM七宗罪之三:在 onViewCreated 中載入資料

語言: CN / TW / HK

theme: smartblue highlight: androidstudio


這是我參與8月更文挑戰的第4天,活動詳情檢視:8月更文挑戰

Jetpack 的 MVVM 本身沒有錯,錯在開發者的某些使用不當。本系列將分享那些 AAC 中常見的錯誤用法,以幫助大家打造更健康的應用架構

ViewModel 資料的首次載入時機?

在 MVVM 中, ViewModel 的重要職責是解耦 View 與 Model。 - View 向 ViewModel 發出指令,請求資料 - View 通過 DataBinding 或 LiveData 等訂閱 ViewModel 的資料變化

關於訂閱 ViewModel 的時機,大家一般放在 onViewCreated ,這是沒有問題的。但是一個常犯的錯誤是將 ViewModel 中首次的資料載入也放到 onViewCreated 中進行: ```kotlin //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
    }

    //請求資料
    viewModel.fetchTaskData(requireArguments().getInt(TASK_ID))
}

} ```

如上,如果 ViewModel 在 onViewCreated 中請求資料,當 View 因為橫豎屏等原因重建時會再次請求,而我們知道 ViewModel 的生命週期長於 View,資料可以跨越 View 的生命週期存在,所以沒有必要隨著 View 的重建反覆請求。

正確的載入時機

ViewModel 的初次資料載入推薦放到 init{} 中進行,這樣可以保證 ViewModelScope 中只加載一次

```kotlin //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 可以在建立 LiveData 的同時進行資料請求,無需建立 MutableLiveData,寫法更簡潔:

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

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

Note: 此種 KTX Builder 只適用於資料僅載入一次的情況,如果後續有使用者動態觸發的資料請求,則還需要藉助 MutableLiveData 來實現。

設定 ViewModel 的初始化引數

如果在 ViewModel 建構函式中請求資料,當需要引數時該如何傳入呢? 比如我們最開頭例子中需要傳入一個 TaskId。

1. 構造引數

最容易想到的方法是通過構造引數傳入。 ```kotlin class DetailTaskViewModel(private val taskId: Int) : ViewModel() {

//...
init {
    viewModelScope.launch {
        _tasks.value = TasksRepository.fetchTask(taskId)
    }
}

}

```

需要注意不能直接呼叫 ViewModel 的建構函式構造,這樣無法將 ViewModel 存入 ViewModelStore

此時需要定義一個 ViewModelProvider.Factory

kotlin 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 建立 ViewModel

```kotlin 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 的引數。 SavedStateHandle 可以幫助 ViewModel 實現資料持久化,同時可以傳遞 Fragment 的 arguments 給 ViewModel。

關於如何使用 SavedStateHandle 對資料進行持久化,由於不是本文重點不做介紹,這裡只展示如何通過 SavedStateHandle 獲取 arguments

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

SavedStateHandle 版本的 ViewModel 定義如下: ```kotlin class TaskViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {

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

} ``` Fragment 中建立 ViewModel 如下:

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

private val viewModel: TaskViewModel by viewModels {
    SavedStateViewModelFactory(
        requireActivity().application,
        requireActivity(),
        arguments// 將arguments作為預設引數傳遞給 SavedStateHandler
    )
}

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

}

```

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

3. 自定義擴充套件方法

前兩種方法的模板程式碼較多,這裡推薦一個自定義的擴充套件方法viewModelByFactory,可以進一步簡化程式碼

```kotlin

typealias CreateViewModel = (handle: SavedStateHandle) -> ViewModel

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

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

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

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

```

使用時的效果如下:

```kotlin

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 以外如果還希望增加更多引數,還可以自定義CreateViewModel`

4. 依賴注入

最後看一下如何使用依賴注入傳參。以 Hilt 為例,Hilt 天然支援 ViewModel 的依賴注入,本質上也是基於 SavedStateHandler 實現的 kotlin @HiltViewModel class DetailedTaskViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle ) : ViewModel() { //... } 新增 @HiltViewModel 註解,並使用 @Inject 註解建構函式。 除了 SavedStateHandle以外,也可以注入其他更多引數

ViewModel 的使用處, 別忘新增 @AndroidEntryPoint ```kotlin @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 的使用,在寫法上最為簡單。

(完)


系列文章

Jetpack MVVM七宗罪之二:在 launchWhenX 中啟動協程

Jetpack MVVM七宗罪之一:拿 Fragment 當 LifecycleOwner