美文网首页
Android进阶教程之手把手教你设计MVVM 架构的Repos

Android进阶教程之手把手教你设计MVVM 架构的Repos

作者: BlueSocks | 来源:发表于2022-04-15 15:38 被阅读0次

    前言

    现在的 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#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 所不具备的。

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

    -END-

    相关文章

      网友评论

          本文标题:Android进阶教程之手把手教你设计MVVM 架构的Repos

          本文链接:https://www.haomeiwen.com/subject/rncqertx.html