Jetpack MVVM七宗罪之三:在 onViewCreated 中載入資料
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
inline fun
fun createViewModelFactoryFactory(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle?,
create: CreateViewModel
): ViewModelProvider.Factory {
return object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun
@PublishedApi
internal fun
```
使用時的效果如下:
```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七宗罪之三:在 onViewCreated 中載入資料
- 為什麼說 Compose 的宣告式程式碼最簡潔 ?Compose/React/Flutter/SwiftUI 語法對比
- Compose 型別穩定性註解:@Stable & @Immutable
- Fragment 這些 API 已廢棄,你還在使用嗎?
- 告別KAPT!使用 KSP 為 Kotlin 編譯提速
- 探索 Jetpack Compose 核心:深入 SlotTable 系統
- 盤點 Material Design 3 帶來的新變化
- Compose 動畫邊學邊做 - 夏日彩虹
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!
- 面試必備:Kotlin 執行緒同步的 N 種方法
- Jetpack MVVM 七宗罪之六:ViewModel 介面暴露不合理
- CreationExtras 來了,建立 ViewModel 的新方式
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼
- 為什麼 RxJava 有 Single / Maybe 等單發資料型別,而 Flow 沒有?