Jetpack MVVM 常见错误三:错误的 ViewModel 数据加载时机

image.png

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容