如何设计 MVVM 架构的 Repository 接口

·  阅读 3387

前言

现在的 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.

Room 对 LiveData 的支持目前也被认为是一个错误

image.png

3. Repo 中使用 LiveData 的弊端

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

image.png 但 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 ,这样的代码才更具有通用性和平台无关性。

4. 为 Repo 提供响应式接口

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

  • 单发请求
  • 流式请求

4.1 单发请求

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

LiveData Builder 需要引入 lifecyce-livedata-ktx

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

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

image.png

由于 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。

4.2 流式请求

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

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

al user = userRepo\
        .getUserLikes()\
        .onStart { \
            // Emit first value\
        }\
        .asLiveData()

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

5. 总结

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

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改