Jetpack MVVM 七宗罪之五: 在 Repository 中使用 LiveData

前言

现在的 Android 项目中几乎少不了对 LiveData 的使用。MVP 时代我们需要定义各种 IXXXView 实现与 Presenter 的通信,而现在已经很少见到类似的接口定义了,大家早已习惯了用响应式的思想设计表现层与逻辑层之间的通信,这少不了 LiveData 的功劳, 因为它够简单好用。但如果将它用在 Domain 甚至 Data 层中就不合适了,但是现实中确实有不少人会这么用。

1. 为什么有人在 Repository 中使用 LiveData ?

当我在同事代码中发现并指出 Repository 中不应使用 LiveData 时,对方会理直气壮的拿官方文档反击我。这可能就是为什么不少人喜欢这样用的原因,因为这曾经是官方文档的推荐做法:


developer.android.com/jetpack/gui…


github.com/android/arc…

上面这些都是曾经在官网文档和Sample中出现过的代码,而且连 Jetpack Room 也对 LiveData 进行了支持,可以为 DAO 生成 LiveData 接口的 API。可见,正是由于官方的推荐,这样的用法才深入人心。

如今的态度

时过境迁,如今官方已经不再这样推荐,而且在最新的文档中对 LiveData 的使用范围做了明确限制,其中特别强调了应该避免在 Repo 中的使用

LiveDatais not designed to handle asynchronous streams of data layer. Even though you can use LiveData transformations and MediatorLiveData to achieve this, this approach has drawbacks: the capability to combine streams of data is very limited and all LiveData objects are observed on the main thread.
developer.android.com/topic/libra…

就连在 Room 中使用 LiveData 也已经认为是一个错误


github.com/cashapp/sql…

2. Repo 中使用 LiveData 的弊端

Google 曾经希望基于 LiveData 实现 MVVM 中 VM 与 M 之间的响应式通信

但 LiveData 的设计初衷只是服务于 View 与 ViewModel 的通信场景,正因为它的职责聚焦所以能力也有限,不适合非 UI 场景下工作,这主要体现在两个方面:

  1. 不支持线程切换
  2. 重度依赖 Lifecycle

不支持线程切换

虽然 LiveData 是个可订阅的对象,但它不像 RxJava 或者 Coroutine Flow 那样具有线程切换的操作符,查看 LiveData 的源码可以发现 observe 只能主线程调用。当我们在 ViewModel 中订阅 Repo 的 LiveData 后,只能在 UI 线程接收数据并进行后续处理。但 ViewModel 更多的是负责逻辑处理,不应该占用主线程宝贵的资源,如果 VM 的逻辑中一旦有耗时操作就会造成 UI 的卡顿。

题外话:VM 中耗时处理本身就是一个不合理的事情,标准的 MVVM 中 VM 的职责应该尽可能简单,更多的业务逻辑应该放到 Model 层或者 Domain 层完成。Model 层不只是简单 API 定义

某些业务逻辑中,我们可能要借助 Transformations#mapTransformations#swichMap 等对 LiveData 做转换处理,而这些默认也是在主线程执行的

class UserRepository {

    // DON'T DO THIS! LiveData objects should not live in the repository.
    fun getUsers(): LiveData<List<User>> {
        ...
    }

    fun getNewPremiumUsers(): LiveData<List<User>> {
        return TransformationsLiveData.map(getUsers()) { users ->
            // This is an expensive call being made on the main thread and may
            // cause noticeable jank in the UI!
            users
                .filter { user ->
                  user.isPremium
                }
          .filter { user ->
              val lastSyncedTime = dao.getLastSyncedTime()
              user.timeCreated > lastSyncedTime
                }
    }
}

复制代码

如上,map { } 在主线程执行,当里面有 getLastSyncedTime 这样的 IO 操作时可能发生 ANR

虽然 LiveData 可以提供了异步 postValue 的能力,但是很多复杂的业务场景中往往需要对数据流进行多段处理。如果要实现所谓的高性能编程,就要求每段处理都能单独指定线程,类似 RxJava 的 observeOn 以及 Flow 的 flowOn 这样的能力,这是 LiveData 所不具备的。

重度依赖 Lifecycle

LiveData 依赖 Lifecycle,而 Lifecycle 是 Android UI 的属性,在非 UI 的场景中使用要么需要自定义 Lifecycle (例如有人会自定义是所谓的 LifecycleAwareViewModel ), 要么使用 LiveData#observerForever(这会造成泄露的风险), Jose Alcérreca 还曾经在 《ViewModels and LiveData: Patterns + AntiPatterns》 一文中推荐使用 Transformations#switchMap 来规避缺少 Lifecycle 的问题。

在我看来这些都不是好的方法,我们不应该对 Lifecycle 有所妥协,在 MVVM 中无论 ViewModel 还是 Model 都应该专注于平台无关的业务逻辑。

一个好的 ViewModel 或者 Repository 应该是一个纯 Java 或 Kotlin 类,不依赖包括 Lifecycle 在内的各种 Andorid 类库,更不应该持有 Context ,这样的代码才更具有通用性和平台无关性。

3. 为 Repo 提供响应式接口

既然 LiveData 不能用,那么如何为 Repo 提供响应式的 API 呢? 从前最常用的当属 RxJava,包括 Retrofit 等常用的三方库对 RxJava 也有友好的支持,如今进入 Kotlin 时代了,我更推荐使用协程。

Repo 中常见的数据请求有两类

  1. 单发请求
  2. 流式请求

单发请求

例如常见的 HTTP 请求中 request 与 response 一一对应。此时可以使用 suspend 函数定义 API,例如使用 LiveData Builder 将其转化为 LiveData

LiveData Builder 需要引入 lifecyce-livedata-ktx

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
复制代码

LiveData Builder 可以在定义 LiveData 的同时提供了调用挂起函数的 CoroutineScope

class UserViewModel(private val userRepo: UserRepository): ViewModel() {
    ...
    val user = liveData { //CoroutineScope
        emit(userRepo.getUser(10))
    }
    ...
}
复制代码

当 LiveData 的 Observer 首次进入 active 状态时协程被启动,当不再有 active 的 Observer 时协程会自动取消,避免泄露。 LiveData Builder 还可以指定 timeoutInMs 参数,延长协程的存活时间

由于 Activity 退到后台造成的 Observer 短时间 inactive,只要不超过 timeoutInMs 协程便不会取消,这保证后台任务的持续执行的同时又避免资源浪费。

Jose Alcérreca 在 《Migrating from LiveData to Kotlin’s Flow》 一文中还推荐了用 StateFlow 替换 ViewModel 的 LiveData 的做法:

class UserViewModel(private val userRepo: UserRepository): ViewModel() {
    ...
    val user = flow { //CoroutineScope
        emit(userRepo.getUser(10))
    }.stateIn(viewModelScope)
    ...
}
复制代码

使用 Flow Builder 构建一个 Flow, 然后使用 stateIn 操作符将其转化为 StateFlow。

流式请求

流式请求常见于观察一个可变的数据源,比如监听数据库的变化等,此时可以使用 Flow 定义响应式 API

ViewModel 中,我们可以将 Repo 中的 Flow 通过 lifecyce-livedata-ktx 的 Flow#asLiveData 转换为一个 LiveData

val user = userRepo
        .getUserLikes()
        .onStart { 
            // Emit first value
        }
        .asLiveData()
复制代码

如果 ViewModel 不使用 LiveData, 那么跟单发请求一样使用 stateIn 转成 StateFlow 即可。

总结

由于 LiveData 简单好用再加上官网早期的推荐,很多人会将 LiveData 用在 Domain 甚至 Data 层等非 UI 场景,这样的用法并不合理,也已经不再被官方推荐。正确做法是应该尽量使用挂起函数或者 Flow 定义 Repo 的 API ,然后在 ViewModel 中合理的调用它们,转成 LiveData 或者 StateFlow 供 UI 层订阅。

更多系列文章

Guess you like

Origin juejin.im/post/7049147565815627789