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

ViewModel 数据的首次加载时机?

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

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

关于订阅 ViewModel 的时机,大家一般放在 onViewCreated ,这是没有问题的。但是一个常犯的错误是将 ViewModel 中首次的数据加载也放到 onViewCreated 中进行:

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

    private val _task = MutableLiveData()
    val task: LiveData = _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 的重建反复请求。

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

正确的加载时机

ViewModel 的初次数据加载推荐放到 init{} 中进行,这样可以保证 ViewModelScope 中只加载一次

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

    private val _tasks = MutableLiveData>()
    val tasks: LiveData> = _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 = 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  create(modelClass: Class): 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(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  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>
): Constructor? {
    for (constructor in modelClass.constructors) {
        val parameterTypes = constructor.parameterTypes
        if (Arrays.equals(signature, parameterTypes)) {
            return constructor as Constructor
        }
    }
    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 的使用,在写法上最为简单。

最后

在这里就还分享一份由大佬亲自收录整理的Android学习PDF+架构视频+面试文档+源码笔记高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料

这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。

当然,你也可以拿去查漏补缺,提升自身的竞争力。

真心希望可以帮助到大家,Android路漫漫,共勉!

如果你有需要的话,只需私信我【进阶】即可获取

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

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

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

展开阅读全文

页面更新:2024-05-23

标签:大佬   进阶   数据   注解   写法   生命周期   架构   函数   持久   时机   加载   定义   参数   方法   资料   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top