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

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

前言

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

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

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

image.png

上面这些都是曾经在官网文档和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 也已经认为是一个错误

image.png

2. Repo 中使用 LiveData 的弊端

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

image.png

但 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

image.png

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 参数,延长协程的存活时间

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。

流式请求

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

image.png

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 层订阅。

更多系列文章

目录
相关文章
|
4月前
|
前端开发 测试技术 API
Jetpack MVVM 七宗罪之六:ViewModel 接口暴露不合理
Jetpack MVVM 七宗罪之六:ViewModel 接口暴露不合理
45 0
|
4月前
|
前端开发 JavaScript Android开发
Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events
Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events
51 0
|
11天前
|
设计模式 前端开发 数据库
构建高效Android应用:使用Jetpack架构组件实现MVVM模式
【4月更文挑战第21天】 在移动开发领域,构建一个既健壮又易于维护的Android应用是每个开发者的目标。随着项目复杂度的增加,传统的MVP或MVC架构往往难以应对快速变化的市场需求和复杂的业务逻辑。本文将探讨如何利用Android Jetpack中的架构组件来实施MVVM(Model-View-ViewModel)设计模式,旨在提供一个更加模块化、可测试且易于管理的代码结构。通过具体案例分析,我们将展示如何使用LiveData, ViewModel, 和Repository来实现界面与业务逻辑的分离,以及如何利用Room数据库进行持久化存储。最终,你将获得一个响应迅速、可扩展且符合现代软件工
15 0
|
23天前
|
存储 设计模式 前端开发
构建高效安卓应用:Jetpack MVVM 架构的实践之路
【4月更文挑战第9天】 在移动开发的迅猛浪潮中,Android 平台以其开放性和灵活性受到开发者青睐。然而,随着应用复杂度的不断增加,传统的开发模式已难以满足快速迭代和高质量代码的双重要求。本文将深入探讨 Jetpack MVVM 架构模式在 Android 开发中的应用实践,揭示如何通过组件化和架构设计原则提升应用性能,实现数据驱动和UI分离,进而提高代码可维护性与测试性。我们将从理论出发,结合具体案例,逐步展开对 Jetpack MVVM 架构的全面剖析,为开发者提供一条清晰、高效的技术实施路径。
|
4月前
|
Android开发 开发者
什么是Android Jetpack,它包括哪些组件?
什么是Android Jetpack,它包括哪些组件?
42 0
|
4月前
|
IDE API 开发工具
Google I/O :Android Jetpack 最新变化(四)Compose
Google I/O :Android Jetpack 最新变化(四)Compose
109 0
|
4月前
|
JSON IDE 测试技术
Google I/O :Android Jetpack 最新变化(二) Performance
Google I/O :Android Jetpack 最新变化(二) Performance
114 0
|
4月前
|
SQL API Android开发
Google I/O :Android Jetpack 最新变化(一) Architecture
Google I/O :Android Jetpack 最新变化(一) Architecture
70 0
|
4月前
|
API Android开发
Google I/O :Android Jetpack 最新变化(三)UI
Google I/O :Android Jetpack 最新变化(三)UI
51 0
|
2月前
|
XML API Android开发
【Android 从入门到出门】第三章:使用Hilt处理Jetpack Compose UI状态
【Android 从入门到出门】第三章:使用Hilt处理Jetpack Compose UI状态
26 4