Jetpack MVVM 七宗罪之六: ViewModel 的接口暴露不合理

在 Jetpack 架构规范中, ViewModel 与 View 之间应该遵循单向数据流的通信方式,Events 永远从 View 流向 VM ,而 State 从 VM 流向 View。

在这里插入图片描述

如果 ViewModel 对 View 暴露了不适当的接口类型,则会破坏单向数据流的形成。不适当的接口类型常见于以下两点:

  1. 暴露 Mutable 状态
  2. 暴露 Suspend 方法

暴露 Mutable 状态

ViewModel 对外暴露的数据状态,无论是 LiveData 或是 StateFlow 都应该使用 Immutable 的接口类型进行暴露而非 Mutable 的具体实现。View 只能单向订阅这些状态的变化,避免对状态反向更新。

class MyViewModel: ViewModel() {
   private val _loading = MutableLiveData<Boolean>()
   val loading: LiveData<Boolean>
       get() = _loading
}

未来避免暴露 Mutable 类型,我们需要像上面这样处理,将 loading 的具体实现定义为一个 private 的 Mutable 类型,便于内部更新。

private val _loading : MutableStateFlow<Boolean?> = MutableStateFlow(null)
val loading = _loading.asStateFlow()

StateFlow 的写法也类似,但是通过 asStateFlow 可以少写一个类型声明,但是要注意此时不要使用 custom get(), 不然 asStateFlow 会执行多次。
每次都要多声明一个带划线的私有变量会让代码显得有些累赘,也正因如此,有 issue 希望 Kotlin 增加类似下面的语法使得对外对内可以暴露不同类型。

//https://youtrack.jetbrains.com/issue/KT-14663
private val loading = MutableLiveData<Boolean>()
        public get(): LiveData<Boolean>

在新语法还未出现的当下,一个让代码变整洁的思路是为 ViewModel 提取对外暴露的抽象类:

abstract class MyViewModel: ViewModel() {
   abstract val loading: LiveData<Boolean>
}

class MyViewModelImpl: MyViewModel() {
   override val loading = MutableLiveData<Boolean>()
   
   fun doSomeWork() {
     // ...
     loading.value = true
   }
}

如上, MyViewModelImpl 内重写的 loading 可以作为 Mutable 类型使用。虽然这种做法会增加了一个抽象类代码量不减反增,但是它使 MyViewModelImpl 内的代码更加简洁,而且对外可以隐藏更多 ViewModel 的实现细节,封装性更好。
但是需要特别注意的是,为了创建 MyViewModel 必须使用自定义 Factory:

val vm : MyViewModel by viewModels { MyViewModelFactory() }

如果你的工程引入了 Hilt ,那么可以通过 @Bind 绑定 ViewModel 的接口与实现,无需自定义 Factory 了,写法跟以前一样,直接使用 by viewModels() 即可

@Module
@InstallIn(ViewModelComponent::class)
abstract class MyViewModule {
    @Binds
    abstract fun MyViewModel(instance: MyViewModelImpl): MyViewModel
}

@HiltViewModel
class MyViewModelImpl @Inject constructor() : MyViewModel()

暴露 Suspend 方法

相对于暴露 Mutable 状态,暴露 Suspend 方法的错误则更为常见。
按照单向数据流的思想 ViewModel 需要提供 API 给 View 用于发送 Events,我们在定义 API 时需要注意避免使用 Suspend 函数,理由如下:

  1. 来自 ViewModel 的数据应该通过订阅 UiState 获取,因此 ViewModel 的其他方法方法不应该有返回值,而 suspend 函数会鼓励返回值的出现。

  2. 理想的 MVVM 中 View 的职责仅仅是渲染 UI,业务逻辑尽量移动到 ViewModel 执行,利于单元测试的同时,ViewModelScope 可以保证一些耗时任务的稳定执行。如果暴露挂起函数给 View,则协程需要在 lifecycleScope 中启动,在横竖屏等场景中会中断任务的进行。

因此,ViewModel 为 View 暴露的 API 应该是非挂起且无法返回值的方法,以下是官网的代码实例:

// DO create coroutines in the ViewModel
class LatestNewsViewModel(
    private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    fun loadNews() {
        viewModelScope.launch {
            val latestNewsWithAuthors = getLatestNewsWithAuthors()
            _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
        }
    }
}

// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
    private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
    // DO NOT do this. News would probably need to be refreshed as well.
    // Instead of exposing a single value with a suspend function, news should
    // be exposed using a stream of data as in the code snippet above.
    suspend fun loadNews() = getLatestNewsWithAuthors()
}

代码中建议暴露一个普通的无返回值的 loadNews ,而 latestNewsWithAuthors 的信息应该通过订阅 LatestNewsUiState 获得 。

有一点让人迷惑的是,官方文档上有这么一句话:

Suspend functions in the ViewModel can be useful if instead of exposing state using a stream of data, only a single value needs to be emitted.
https://developer.android.com/kotlin/coroutines/coroutines-best-practices#viewmodel-coroutines

对于单发数据的请求允许使用挂起函数返回。但我建议大家忘掉这句话,理由有两点:

  1. 挂起函数的口子一开就容易不分场景的滥用,如果整体数据流结构造成破坏反而因小失大,索性应该从源头禁止

  2. 理论上来说,UI 上不存在单发数据请求的必要性,完全可以通过良好的设计转化成 UiState ,这也更符合响应式的编程模型。

更多阅读:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Jetpack是一个结合MVVM的快速开发框架,它基于MVVM模式并集成了谷歌官方推荐的Jetpack组件库,包括LiveData、ViewModel、Lifecycle和Navigation组件。这个框架使用Kotlin语言,并添加了大量的拓展函数,以简化代码。它还集成了Retrofit网络请求和协程,可以帮助开发者快速开发项目。\[1\] MVVM是一种软件架构模式,它将应用程序分为三个主要部分:模型(Model)、视图(View)和视图模型(ViewModel)。在MVVM中,视图负责显示数据和用户交互,模型负责处理数据和业务逻辑,而视图模型则充当视图和模型之间的中间层,负责管理视图的状态和数据。JetpackMVVM模式可以帮助开发者更好地组织和管理代码,提高开发效率和舒适度。\[1\] 使用JetpackMVVM可以带来许多好处,例如简化代码、提高开发效率、提供更好的代码结构和可维护性。通过使用Jetpack的组件库,开发者可以更轻松地处理生命周期管理、数据共享和导航等常见任务。而MVVM模式则可以帮助开发者更好地分离关注点,使代码更易于测试和维护。\[2\] 总之,JetpackMVVM是一种强大的组合,可以帮助开发者快速开发Android应用程序,并提供更好的代码结构和可维护性。如果你想了解更多关于JetpackMVVM的信息,可以参考引用\[1\]中提供的Jetpack框架的介绍。 #### 引用[.reference_title] - *1* *2* [JetpackMvvm](https://blog.csdn.net/u014608640/article/details/124711159)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid...](https://blog.csdn.net/m0_37796683/article/details/130277908)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值