查看原文
其他

Jetpack MVVM七宗罪 之三 :在 onViewCreated 中请求数据

fundroid AndroidPub 2022-07-13

Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,以帮助大家打造更健康的应用架构

ViewModel 数据的首次加载时机?

在 MVVM 中, ViewModel 的重要职责是解耦 View 与 Model。

  • View 向 ViewModel 发出指令,请求数据
  • View 通过 DataBinding 或 LiveData 等订阅 ViewModel 的数据变化

关于订阅 ViewModel 的时机,大家一般放在 onViewCreated ,这是没有问题的。但是一个常犯的错误是将 ViewModel 中首次的数据加载也放到 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
        }

        //请求数据
        viewModel.fetchTaskData(requireArguments().getInt(TASK_ID))
    }
}

如上,如果 ViewModel 在 onViewCreated 中请求数据,当 View 因为横竖屏等原因重建时会再次请求,而我们知道 ViewModel 的生命周期长于 View,数据可以跨越 View 的生命周期存在,所以没有必要随着 View 的重建反复请求。

正确的加载时机

ViewModel 的初次数据加载推荐放到 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 可以在创建 LiveData 的同时进行数据请求,无需创建 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 只适用于数据仅加载一次的情况,如果后续有用户动态触发的数据请求,则还需要借助 MutableLiveData 来实现。

设置 ViewModel 的初始化参数

如果在 ViewModel 构造函数中请求数据,当需要参数时该如何传入呢?比如我们最开头例子中需要传入一个 TaskId。

1. 构造参数

最容易想到的方法是通过构造参数传入。

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

需要注意不能直接调用 ViewModel 的构造函数构造,这样无法将 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 创建 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 的参数。SavedStateHandle 可以帮助 ViewModel 实现数据持久化,同时可以传递 Fragment 的 arguments 给 ViewModel。

关于如何使用 SavedStateHandle 对数据进行持久化,由于不是本文重点不做介绍,这里只展示如何通过 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 中创建 ViewModel 如下:

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,可以进一步简化代码


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 以外如果还希望增加更多参数,还可以自定义 CreateViewModel

4. 依赖注入

最后看一下如何使用依赖注入传参。以 Hilt 为例,Hilt 天然支持 ViewModel 的依赖注入,本质上也是基于 SavedStateHandler 实现的

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

添加 @HiltViewModel 注解,并使用 @Inject 注解构造函数。除了 SavedStateHandle以外,也可以注入其他更多参数

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 的使用,在写法上最为简单。


~ FIN ~


系列文章:
Jetpack MVVM七宗罪 之二 :使用 luanchWhenX 启动协程
Jetpack MVVM七宗罪 之一 :还在使用 Fragment 作为 LifecycleOwner ?


加好友拉你进群,技术干货聊不停


↓关注公众号↓↓添加微信交流↓


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存