Jetpack MVVM七宗罪之三:在 onViewCreated 中加载数据

简介: Jetpack MVVM七宗罪之三:在 onViewCreated 中加载数据

ViewModel 数据的首次加载时机?

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

  • View 向 ViewModel 发出指令,请求数据
  • View 通过 DataBinding 或 LiveData 等订阅 ViewModel 的数据变化
  • image.png
  • 关于订阅 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 的重建反复请求。

image.png

正确的加载时机

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

(完)

目录
相关文章
|
6月前
|
前端开发 测试技术 API
Jetpack MVVM 七宗罪之六:ViewModel 接口暴露不合理
Jetpack MVVM 七宗罪之六:ViewModel 接口暴露不合理
83 0
|
6月前
|
前端开发 Java API
Jetpack MVVM 七宗罪之五: 在 Repository 中使用 LiveData
Jetpack MVVM 七宗罪之五: 在 Repository 中使用 LiveData
115 0
|
22天前
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
6月前
|
前端开发 JavaScript Android开发
Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events
Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events
144 0
|
6月前
|
前端开发 Android开发
Android架构组件JetPack之DataBinding玩转MVVM开发实战(四)
Android架构组件JetPack之DataBinding玩转MVVM开发实战(四)
Android架构组件JetPack之DataBinding玩转MVVM开发实战(四)
|
6月前
|
存储 设计模式 前端开发
构建高效安卓应用:Jetpack MVVM 架构的实践之路
【4月更文挑战第9天】 在移动开发的迅猛浪潮中,Android 平台以其开放性和灵活性受到开发者青睐。然而,随着应用复杂度的不断增加,传统的开发模式已难以满足快速迭代和高质量代码的双重要求。本文将深入探讨 Jetpack MVVM 架构模式在 Android 开发中的应用实践,揭示如何通过组件化和架构设计原则提升应用性能,实现数据驱动和UI分离,进而提高代码可维护性与测试性。我们将从理论出发,结合具体案例,逐步展开对 Jetpack MVVM 架构的全面剖析,为开发者提供一条清晰、高效的技术实施路径。
|
6月前
|
设计模式 前端开发 数据库
构建高效Android应用:使用Jetpack架构组件实现MVVM模式
【4月更文挑战第21天】 在移动开发领域,构建一个既健壮又易于维护的Android应用是每个开发者的目标。随着项目复杂度的增加,传统的MVP或MVC架构往往难以应对快速变化的市场需求和复杂的业务逻辑。本文将探讨如何利用Android Jetpack中的架构组件来实施MVVM(Model-View-ViewModel)设计模式,旨在提供一个更加模块化、可测试且易于管理的代码结构。通过具体案例分析,我们将展示如何使用LiveData, ViewModel, 和Repository来实现界面与业务逻辑的分离,以及如何利用Room数据库进行持久化存储。最终,你将获得一个响应迅速、可扩展且符合现代软件工
90 0
|
6月前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
5月前
|
数据管理 API 数据库
探索Android Jetpack:现代安卓开发的利器
Android Jetpack是谷歌为简化和优化安卓应用开发而推出的一套高级组件库。本文深入探讨了Jetpack的主要构成及其在应用开发中的实际运用,展示了如何通过使用这些工具来提升开发效率和应用性能。
|
4月前
|
存储 数据库 Android开发
🔥Android Jetpack全解析!拥抱Google官方库,让你的开发之旅更加顺畅无阻!🚀
【7月更文挑战第28天】在Android开发中追求高效稳定的路径?Android Jetpack作为Google官方库集合,是你的理想选择。它包含多个独立又协同工作的库,覆盖UI到安全性等多个领域,旨在减少样板代码,提高开发效率与应用质量。Jetpack核心组件如LiveData、ViewModel、Room等简化了数据绑定、状态保存及数据库操作。引入Jetpack只需在`build.gradle`中添加依赖。例如,使用Room进行数据库操作变得异常简单,从定义实体到实现CRUD操作,一切尽在掌握之中。拥抱Jetpack,提升开发效率,构建高质量应用!
66 4