MVVM框架中Kotlin Flow的实践

38 篇文章 3 订阅
2 篇文章 0 订阅

作者:少冰半糖柠檬茶

前言

在 Google Android 团队宣布了 Jetpack 的视图模型之后,MVVM 架构已经成为了 Android 开发最流行的架构之一。如下图所示:

不过在 Google 的前期官方文档中,其 Repository 层是直接使用 LiveData 的,而且连 Jetpack Room 也对 LiveData 进行了支持,接口可以直接返回 LiveData 的数据。所以在很长一段时间内,各种开源的 MVVM 框架或者博客中,也是在 Repository 层中直接使用 LiveData。

这里,我们就会有疑问:Repository 层为什么使用 LiveData 呢?(因为通过官方文档介绍,LiveData 应该要跟Acvtivity 、Fragment 这类UI组件有关系,需要依赖 Lifecycle,放在 Repository 层非常奇怪)。

那么正确的做法是什么呢?下面将会演示基于 LiveData 实践的 MVVM框架、其存在的弊端、以及基于 Flow 实践的 MVVM框架,然后通过引入 Flow 来解决 LiveData 存在的问题。

在 Repository 层使用 LiveData 的 MVVM 实践

首先将这张 MVVM 框架图细化,来看看每个层级间的数据类型和数据流向:

然后再进一步细化,来看下设计细节:

1. 数据处理流程:

  1. 通过基础网络库(类似于:LibNetwork, 一般是业务方对Retrofit的封装)获取网络数据

  2. 在 Repository 层将请求数据转化为 LiveData<RepositoryData<T>>,请求场景分多种:

    • 网络请求
    • 本地请求,通常指 Room 数据库
    • 网络请求 + 本地请求,用于先显示本地数据,在请求网络数据成功后刷新界面的场景
  3. 在 ViewModel 层将数据 Transfomer 为 UI层 所能理解的 VO 数据,也就是 LiveData<RepositoryData<S>>

  4. 在 UI 层监听 LiveData 数据的变化

2. 使用方法

以下是以 请求网络数据 为例:

UI 层:

    private val dailyMottoViewModel by viewModels<DailyMottoViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = CommunicationsampleActivityLaunchTargetBinding.inflate(layoutInflater)
        setContentView(binding.root)

        dailyMottoViewModel.dailyMottoLiveData.observe(this) {
            when {
                it.isLoading() -> {
                    // show loading ui
                }
                it.isSuccess() -> {
                    // show success ui
                }
                it.isError() -> {
                    // show error ui
                }
            }
        }
        dailyMottoViewModel.requestDailyMotto()
    }

ViewModel 层:

class DailyMottoViewModel : BaseViewModel() {

    private val dailyMottoRepository by lazyRepository<DailyMottoRepository>()

    private val dailyMottoMutableLiveData = MutableLiveData<RepositoryData<DailyMottoVO>>()
    val dailyMottoLiveData
        get() = dailyMottoMutableLiveData

    fun requestDailyMotto() {
        dailyMottoMutableLiveData.observeData(
            dataSource = dailyMottoRepository.getDailyMotto(),
            transformer = {
                it.transformToVO()
            }
        )
    }
}

Repository 层:

class DailyMottoRepository : BaseRepository() {

    fun getDailyMotto(): LiveData<RepositoryData<DailyMottoModel>> {
        return fetchNetworkData {
            WebServiceFactory.instance.fetchDailyMotto()
        }
    }
}

3. Repository 层实现原理

    fun <T> fetchNetworkData(requestFun: suspend () -> WebResponse<T>, saveToLocal: ((T) -> Unit)? = null): LiveData<RepositoryData<T>> {
        val liveData = MutableLiveData<RepositoryData<T>>()
        repositoryScope.launch {
            // Loading 状态
            liveData.postValue(RepositoryData.loading())

            val result = invokeFunction(requestFun)
            if (result.isSuccessful()) {
                // 是否需要将数据保存到本地,通常会缓存到 Room 数据库中
                saveToLocal?.let { saveDataInLocal ->
                    withContext(Dispatchers.IO) {
                        result.data?.let { saveDataInLocal.invoke(it) }
                    }
                }
                // 数据请求成功
                liveData.postValue(RepositoryData.success(result.data))
            } else {
                // 数据请求失败
                liveData.postValue(RepositoryData.error(RepositoryData.MSG_SERVER_ERROR, statusCode = result.code))

                // 处理通用请求异常:例如 token失效、鉴权等
                if (RepositoryData.isSpecificErrorToken(result.code)) {
                    onTokenError(result.code)
                }
            }
        }
        return liveData
    }

    private suspend fun <T> invokeFunction(function: suspend () -> WebResponse<T>): WebResponse<T> = withContext(Dispatchers.IO) {
        val response: WebResponse<T> =
            try {
                function.invoke()
            } catch (ex: Exception) {
                XLog.e(TAG, "invokeFunction: ${ex.message}")
                WebResponse(code = -1)
            }
        response
    }

Repository 中使用 LiveData 的弊端

LiveData API设计得过于简单,难以应对Repository层可能出现的许多复杂的数据处理场景。主要体现在以下三个方面:

  1. 不支持线程切换
  2. 不支持背压处理
  3. 重度依赖 Lifecycle

不支持线程切换

在复杂的业务场景中,往往伴随着线程切换来对数据进行多次处理,类似 RxJava 的 observeOn 以及 Flow 的 flowOn,而 LiveData 并没有这种能力。所以只能通过 协程 来进行线程切换,而在 Repository 层,就只能自定义 repositoryScope 并处理协程取消的逻辑。即:

    private var repositoryScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
        get() {
            if (field.coroutineContext[Job]?.isActive == true) {
                return field
            }
            val newScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
            repositoryScope = newScope
            return newScope
        }

    fun cancel() {
        repositoryScope.cancel()
    }

不支持背压处理

LiveData 肩负着为 UI 提供数据订阅的能力,所以他的数据订阅只能在主线程,虽然可以在子线程通过 postValue 去发布数据,但短期内调用 postValue 过快,由于没有背压处理,只保留最新的数据,因此可能造成预期之外的数据丢失问题。

而 Flow 则拥有完整的背压策略,可应对 Repository 层可能出现的各种复杂数据场景。

重度依赖 Lifecycle

LiveData 依赖 Lifecycle,具有生命周期感知能力,遵循 activity 和 fragment 等实体的生命周期,在非 UI 的场景中使用要么需要自定义 Lifecycle , 要么使用 LiveData#observerForever(会造成泄露的风险)。在上面的案例中,ViewModel 需要监听 Repositoy 层的 LiveData,就必须特殊处理,避免内存泄露的问题。例如:

    private val tempLiveDataList = mutableMapOf<LiveData<*>, Observer<*>>()

    /**
     * dataSource -> [transformer] -> LiveData
     */
    fun <T, D> MutableLiveData<RepositoryData<T>>.observeData(
        dataSource: LiveData<RepositoryData<D>>,
        transformer: (D) -> T
    ) {
        val data = MediatorLiveData<RepositoryData<T>>()
        data.addSource(dataSource) {
            when {
                it.isError() -> checkPostErrorValue(it, transformer)
                it.isLoading() -> checkPostLoadingValue(it, transformer)
                else -> value = if (it.data == null) RepositoryData.success(it.data) else
                    RepositoryData.success(transformer.invoke(it.data))
            }
        }
        data.observeForever(Observer<RepositoryData<T>> { }.apply { tempLiveDataList[data] = this })
    }

    /**
     * 在 onCleared 中清除 Observer, 避免泄露
     */
    override fun onCleared() {
        super.onCleared()
        tempLiveDataList.forEach {
            it.key.removeObserver(it.value as Observer<in Any>)
        }
    }

在 Repository 层使用 Flow 的 MVVM 实践

使用 Flow 来替换 Repository 层中 LiveData 使用,主要涉及到 ViewModel 层和 Repository 层基础类的修改,而修改后的逻辑更加简洁、易读。而且官方文档也有所更新,对 LiveData 的使用场景有所限制,见:developer.android.com/topic/libra…

https://developer.android.com/topic/libraries/architecture/livedata#livedata-in-architecture

It may be tempting to work LiveData objects in your data layer class, but LiveDatais not designed to handle asynchronous streams of data. 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 (including ones created through transformations) are observed on the main thread. The code below is an example of how holding a LiveData in the Repository can block the main thread: If you need to use streams of data in other layers of your app, consider using Kotlin Flows and then converting them to LiveData in the ViewModel using asLiveData()
. Learn more about using Kotlin Flow with LiveData in this codelab . For codebases built with Java, consider using Executors in conjuction with callbacks or RxJava.

1. Repository 层实现原理

    protected fun <T> fetchNetworkData(saveToLocal: ((T) -> Unit)? = null, requestFun: suspend () -> WebResponse<T>): Flow<RepositoryData<T>> {
        return flow<RepositoryData<T>> {
            // Loading 状态
            emit(RepositoryData.loading())

            val webResponse = requestFun.invoke()
            if (webResponse.isSuccessful()) {
                // 是否需要将数据保存到本地,通常会缓存到 Room 数据库中
                webResponse.data?.let { saveToLocal?.invoke(it) }
                // 数据请求成功
                emit(RepositoryData.success(webResponse.data))
            } else {
                // 处理通用请求异常:例如 token失效、鉴权等
                if (RepositoryData.isSpecificErrorToken(webResponse.code)) {
                    onTokenError(webResponse.code)
                }
                // 数据请求失败
                emit(RepositoryData.error(webResponse.msg, webResponse.data, webResponse.code))
            }

        }.flowOnIOWithCatch()
    }

    private fun <T> Flow<RepositoryData<T>>.flowOnIOWithCatch(): Flow<RepositoryData<T>> {
        return this.catch {
            emit(RepositoryData.error("local data error with catch"))
        }.flowOn(Dispatchers.IO)
    }

2. ViewModel 层实现原理

    /**
     * dataSource -> [transformer] -> LiveData
     */
    fun <T, D> MutableLiveData<RepositoryData<T>>.observeData(
        dataSource: Flow<RepositoryData<D>>,
        transformer: (D) -> T
    ) {
        dataSource.collectInLaunch {
            when {
                it.isError() -> checkPostErrorValue(it, transformer)
                it.isLoading() -> checkPostLoadingValue(it, transformer)
                else -> value = if (it.data == null) RepositoryData.success(it.data) else
                    RepositoryData.success(transformer.invoke(it.data))
            }
        }
    }

    private inline fun <T> Flow<T>.collectInLaunch(crossinline action: suspend (value: T) -> Unit) = viewModelScope.launch {
        collect {
            action.invoke(it)
        }
    }

3. 使用方法

由于 API 设计一致,使用方法与之前没有任何更改,因此可以无缝切换。唯一变更点就是 Repository 层的返回数据类型由 LiveData 修改为 Flow:

class DailyMottoRepository : BaseRepository() {

    fun getDailyMotto(): Flow<RepositoryData<DailyMottoModel>> {
        return fetchNetworkData {
            WebServiceFactory.instance.getDailyMotto()
        }
    }
}

总结

综上,可以在Repository层使用Flow获取数据,并且Retrofit、Room都有自带的Flow扩展支持,使用上基本无缝衔接;ViewModel层collect来自Repository层的Flow,进行数据转换,将Model转到VO,再利用LiveData进行UI更新。

这时候就有另外一个问题了,既然 Flow 这么好用,可以在 Repository 层中替换 LiveData 的使用,那么要不要也在 ViewModel 层中使用 Flow ,完全跟 LiveData 说再见呢?这个问题就留待大家思考吧

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值