MVVM框架中Kotlin Flow的实践

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MVVM框架中Kotlin Flow的实践相关的知识,希望对你有一定的参考价值。

作者:少冰半糖柠檬茶

前言

在 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 说再见呢?这个问题就留待大家思考吧

以上是关于MVVM框架中Kotlin Flow的实践的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 协程Flow 异步流 ① ( 以异步返回返回多个返回值 | 同步调用返回多个值的弊端 | 尝试在 sequence 中调用挂起函数返回多个返回值 | 协程中调用挂起函数返回集合 )

Kotlin 协程Flow 异步流 ① ( 以异步返回返回多个返回值 | 同步调用返回多个值的弊端 | 尝试在 sequence 中调用挂起函数返回多个返回值 | 协程中调用挂起函数返回集合 )

译LiveData-Flow在MVVM中的最佳实践

Kotlin flow实践总结

用Kotlin Flow解决Android开发中的痛点问题

用Kotlin Flow解决Android开发中的痛点问题