如何设计 MVVM 架构的 Repository 接口
前言
现在的 Android 项目中几乎少不了对 LiveData 的使用。MVP 时代我们需要定义各种 IXXXView 实现与 Presenter 的通信,而现在已经很少见到类似的接口定义了,大家早已习惯了用响应式的思想设计表现层与逻辑层之间的通信,这少不了 LiveData 的功劳, 因为它够简单好用。但如果将它用在 Domain 甚至 Data 层中就不合适了,但是现实中确实有不少人会这么用。
1. 为什么有人在 Repository 中使用 LiveData ?
当我在 review 他人代码时如果发现了 Repository 中使用了 LiveData,一般会作为问题指出,但有时对方会以官方的推荐为理由来反击我:
比如上面这段代码就来自曾经的官方文档,而且 Room 这样的第一方组件也对 LiveData 进行了支持。可能就是这一系列官方有意无意的背书,让不少人乐于在数据层的相关代码中使用 LiveData
2. 官方究竟是什么态度?
以前 Google 官方对于 LiveData 的使用确实比较随意,但在最新的官方文档中,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.
-- https://developer.android.com/topic/libraries/architecture/livedata#livedata-in-architecture
Room 对 LiveData 的支持目前也被认为是一个错误
3. Repo 中使用 LiveData 的弊端
Google 曾经希望基于 LiveData 实现 MVVM 中 VM 与 M 之间的响应式通信
但 LiveData 的设计初衷只是服务于 View 与 ViewModel 的通信场景,正因为它的职责聚焦所以能力也有限,不适合非 UI 场景下工作,这主要体现在两个方面:
不支持线程切换 重度依赖 Lifecycle
3.1 不支持线程切换
虽然 LiveData 是个可订阅的对象,但它不像 RxJava 或者 Coroutine Flow 那样具有线程切换的操作符,查看 LiveData 的源码可以发现 observe
只能主线程调用。当我们在 ViewModel 中订阅 Repo 的 LiveData 后,只能在 UI 线程接收数据并进行后续处理。但 ViewModel 更多的是负责逻辑处理,不应该占用主线程宝贵的资源,如果 VM 的逻辑中一旦有耗时操作就会造成 UI 的卡顿。
“题外话:VM 中耗时处理本身就是一个不合理的事情,标准的 MVVM 中 VM 的职责应该尽可能简单,更多的业务逻辑应该放到 Model 层或者 Domain 层完成。Model 层不只是简单 API 定义
某些业务逻辑中,我们可能要借助 Transformations#map
和 Transformations#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 所不具备的。
3.2 重度依赖 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 中常见的数据请求有两类
单发请求 流式请求
3.1 单发请求
例如常见的 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。
3.2 流式请求
流式请求常见于观察一个可变的数据源,比如监听数据库的变化等,此时可以使用 Flow 定义响应式 API
ViewModel 中,我们可以将 Repo 中的 Flow 通过 lifecyce-livedata-ktx 的 Flow#asLiveData
转换为一个 LiveData
val user = userRepo
.getUserLikes()
.onStart {
// Emit first value
}
.asLiveData()
如果 ViewModel 不使用 LiveData, 那么跟单发请求一样使用 stateIn
转成 StateFlow 即可。
4. 总结
由于 LiveData 的简单好用,很多人会将 LiveData 用在 Domain 甚至 Data 层等非 UI 场景,这样的用法并不合理,也已经不再被官方推荐。正确做法是应该尽量使用挂起函数或者 Flow 定义 Repo 的 API ,然后在 ViewModel 中合理的调用它们,转成 LiveData 或者 StateFlow 供 UI 层订阅。
推荐文章