首页 > 其他分享 >Kotlin flow实践总结

Kotlin flow实践总结

时间:2023-06-22 14:01:33浏览次数:21  
标签:总结 fetchDataFromNetWork Kotlin flow data collect 加载 数据 CResult


背景

最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。

Kotlin flow实践总结_java

Flow是什么

按顺序发出多个值的数据流。
本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。

Kotlin flow实践总结_java_02

  • 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开始发射数据流。
    生产者与消费者是一对一的关系。当生产者发送数据的时候,对应的消费者才可以收到数据。
  • 热流:不管有没有执行collect(也就是不管有没有消费者),生产者都会发射数据流到内存中。
    生产者与消费者是一对多的关系。当生产者发送数据的时候,多个消费者都可以收到数据

实践场景

场景一:简单列表数据的加载状态

简单的列表显示场景,可以使用onStart,onEmpty,catch,onCompletion等回调操作符,监听数据流的状态,显示相应的加载状态UI。

  • onStart:在数据发射之前触发,onStart所在的线程,是数据产生的线程
  • onCompletion:在数据流结束时触发,onCompletion所在的线程,是数据产生的线程
  • onEmpty:当数据流结束了,缺没有发出任何元素的时候触发。
  • catch:数据流发生错误的时候触发
  • flowOn:指定上游数据流的CoroutineContext,下游数据流不会受到影响
private fun coldFlowDemo() {
    //创建一个冷流,在3秒后发射一个数据
    val coldFlow = flow<Int> {
        delay(3000)
        emit(1)
    }
    lifecycleScope.launch(Dispatchers.IO) {
        coldFlow.onStart {
            Log.d(TAG, "coldFlow onStart, thread:${Thread.currentThread().name}")
            mBinding.progressBar.isVisible = true
            mBinding.tvLoadingStatus.text = "加载中"
        }.onEmpty {
            Log.d(TAG, "coldFlow onEmpty, thread:${Thread.currentThread().name}")
            mBinding.progressBar.isVisible = false
            mBinding.tvLoadingStatus.text = "数据加载为空"
        }.catch {
            Log.d(TAG, "coldFlow catch, thread:${Thread.currentThread().name}")
            mBinding.progressBar.isVisible = false
            mBinding.tvLoadingStatus.text = "数据加载错误:$it"
        }.onCompletion {
            Log.d(TAG, "coldFlow onCompletion, thread:${Thread.currentThread().name}")
            mBinding.progressBar.isVisible = false
            mBinding.tvLoadingStatus.text = "加载完成"
        }
            //指定上游数据流的CoroutineContext,下游数据流不会受到影响
            .flowOn(Dispatchers.Main)
            .collect {
                Log.d(TAG, "coldFlow collect:$it, thread:${Thread.currentThread().name}")
            }
    }
}

比如上面的例子。 使用flow构建起函数,创建一个冷流,3秒后发送一个值到数据流中。 使用onStart,onEmpty,catch,onCompletion操作符,监听数据流的状态。

日志输出:

coldFlow onStart, thread:main
coldFlow onCompletion, thread:main
coldFlow collect:1, thread:DefaultDispatcher-worker-1

场景二:同一种数据,需要加载本地数据和网络数据

在实际的开发场景中,经常会将一些网络数据保存到本地,下次加载数据的时候,优先使用本地数据,再使用网络数据。
但是本地数据和网络数据的加载完成时机不一样,所以可能会有下面几种场景。

  1. 本地数据比网络数据先加载完成:那先使用本地数据,再使用网络数据
  2. 网络数据比本地数据先加载完成:
  • 网络数据加载成功,那只使用网络数据即可,不需要再使用本地数据了。
  • 网络数据加载失败,可以继续尝试使用本地数据进行兜底。
  1. 本地数据和网络数据都加载失败:通知上层数据加载失败

实现CacheRepositity

将上面的逻辑进行简单封装成一个基类,CacheRepositity。
相应的子类,只需要实现两个方法即可。

  • CResult:代表加载结果,Success 或者 Error。
  • fetchDataFromLocal(),实现本地数据读取的逻辑
  • fetchDataFromNetWork(),实现网络数据获取的逻辑
abstract class CacheRepositity<T> {
    private val TAG = "CacheRepositity"

    fun getData() = channelFlow<CResult<T>> {
        supervisorScope {
            val dataFromLocalDeffer = async {
                fetchDataFromLocal().also {
                    Log.d(TAG,"fetchDataFromLocal result:$it , thread:${Thread.currentThread().name}")
                    //本地数据加载成功  
                    if (it is CResult.Success) {
                        send(it)
                    }
                }
            }

            val dataFromNetDeffer = async {
                fetchDataFromNetWork().also {
                    Log.d(TAG,"fetchDataFromNetWork result:$it , thread:${Thread.currentThread().name}")
                    //网络数据加载成功  
                    if (it is CResult.Success) {
                        send(it)
                        //如果网络数据已加载,可以直接取消任务,就不需要处理本地数据了
                        dataFromLocalDeffer.cancel()
                    }
                }
            }

            //本地数据和网络数据,都加载失败的情况
            val localData = dataFromLocalDeffer.await()
            val networkData = dataFromNetDeffer.await()
            if (localData is CResult.Error && networkData is CResult.Error) {
                send(CResult.Error(Throwable("load data error")))
            }
        }
    }

    protected abstract suspend fun fetchDataFromLocal(): CResult<T>

    protected abstract suspend fun fetchDataFromNetWork(): CResult<T>

}

sealed class CResult<out R> {
    data class Success<out T>(val data: T) : CResult<T>()
    data class Error(val throwable: Throwable) : CResult<Nothing>()
}

测试验证

写个TestRepositity,实现CacheRepositity的抽象方法。
通过delay延迟耗时来模拟各种场景,观察日志的输出顺序。

private fun cacheRepositityDemo(){
    val repositity=TestRepositity()
    lifecycleScope.launch {
        repositity.getData().onStart {
            Log.d(TAG, "TestRepositity: onStart")
        }.onCompletion {
            Log.d(TAG, "TestRepositity: onCompletion")
        }.collect {
            Log.d(TAG, "collect: $it")
        }
    }
}
本地数据比网络数据加载快
class TestRepositity : CacheRepositity<String>() {
    override suspend fun fetchDataFromLocal(): CResult<String> {
        delay(1000)
        return CResult.Success("data from fetchDataFromLocal")
    }

    override suspend fun fetchDataFromNetWork(): CResult<String> {
        delay(2000)
        return CResult.Success("data from fetchDataFromNetWork")
    }
}

模拟数据:本地加载delay1秒,网络加载delay2秒
日志输出:collect 执行两次,先收到本地数据,再收到网络数据。

onStart
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion
网络数据比本地数据加载快
class TestRepositity : CacheRepositity<String>() {
    override suspend fun fetchDataFromLocal(): CResult<String> {
        delay(2000)
        return CResult.Success("data from fetchDataFromLocal")
    }

    override suspend fun fetchDataFromNetWork(): CResult<String> {
        delay(1000)
        return CResult.Success("data from fetchDataFromNetWork")
    }
}

模拟数据:本地加载delay 2秒,网络加载delay 1秒
日志输出:collect 只执行1次,只收到网络数据。

onStart
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion
网络数据加载失败,使用本地数据
class TestRepositity : CacheRepositity<String>() {
    override suspend fun fetchDataFromLocal(): CResult<String> {
        delay(2000)
        return CResult.Success("data from fetchDataFromLocal")
    }

    override suspend fun fetchDataFromNetWork(): CResult<String> {
        delay(1000)
        return CResult.Error(Throwable("fetchDataFromNetWork Error"))
    }
}

模拟数据:本地加载delay 2秒,网络数据加载失败
日志输出:collect 只执行1次,只收到本地数据。

onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
onCompletion
网络数据和本地数据都加载失败
class TestRepositity : CacheRepositity<String>() {
    override suspend fun fetchDataFromLocal(): CResult<String> {
        delay(2000)
        return CResult.Error(Throwable("fetchDataFromLocal Error"))
    }

    override suspend fun fetchDataFromNetWork(): CResult<String> {
        delay(1000)
        return CResult.Error(Throwable("fetchDataFromNetWork Error"))
    }
}

模拟数据:本地数据加载失败,网络数据加载失败
日志输出: collect 只执行1次,结果是CResult.Error,代表加载数据失败。

onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Error(throwable=java.lang.Throwable: fetchDataFromLocal Error) , thread:main
collect: Error(throwable=java.lang.Throwable: load data error)
onCompletion

场景三:多种数据源,按照顺序合并进行展示

Kotlin flow实践总结_java_03

在实际的开发场景中,经常一个页面的数据,是需要发起多个网络请求之后,组合数据之后再进行显示。 比如类似这种页面,3种数据,需要由3个网络请求获取得到,然后再进行相应的显示。

实现目标:

  1. 接口间不需要互相等待,哪些数据先回来,就先展示哪部分
  2. 控制数据的显示顺序
flow combine操作符

可以合并多个不同的 Flow 数据流,生成一个新的流。 只要其中某个子 Flow 数据流有产生新数据的时候,就会触发 combine 操作,进行重新计算,生成一个新的数据。

例子
class HomeViewModel : ViewModel() {

    //暴露给View层的列表数据
    val list = MutableLiveData<List<String?>>()

    //多个子Flow,这里简单都返回String,实际场景根据需要,返回相应的数据类型即可
    private val bannerFlow = MutableStateFlow<String?>(null)
    private val channelFlow = MutableStateFlow<String?>(null)
    private val listFlow = MutableStateFlow<String?>(null)


    init {
        //使用combine操作符
        viewModelScope.launch {
            combine(bannerFlow, channelFlow, listFlow) { bannerData, channelData, listData ->
                Log.d("HomeViewModel", "combine  bannerData:$bannerData,channelData:$channelData,listData:$listData")
                //只要子flow里面的数据不为空,就放到resultList里面
                val resultList = mutableListOf<String?>()
                if (bannerData != null) {
                    resultList.add(bannerData)
                }
                if (channelData != null) {
                    resultList.add(channelData)
                }
                if (listData != null) {
                    resultList.add(listData)
                }
                resultList
            }.collect {
                //收集combine之后的数据,修改liveData的值,通知UI层刷新列表
                Log.d("HomeViewModel", "collect: ${it.size}")
                list.postValue(it)
            }
        }
    }

    fun loadData() {
        viewModelScope.launch(Dispatchers.IO) {
            //模拟耗时操作
            async {
                delay(1000)
                Log.d("HomeViewModel", "getBannerData success")
                bannerFlow.emit("Banner")
            }
            async {
                delay(2000)
                Log.d("HomeViewModel", "getChannelData success")
                channelFlow.emit("Channel")
            }
            async {
                delay(3000)
                Log.d("HomeViewModel", "getListData success")
                listFlow.emit("List")
            }
        }
    }
}

HomeViewModel

  1. 提供一个 LiveData 的列表数据给View层使用
  2. 内部有3个子 flow ,分别负责相应数据的生产。(这里简单都返回String,实际场景根据需要,返回相应的数据类型即可)。
  3. 通过 combine 操作符,组合这3个子flow的数据。
  4. collect 接收生成的新数据,并修改liveData的数据,通知刷新UI

View层使用

private fun flowCombineDemo() {
    val homeViewModel by viewModels<HomeViewModel>()
    homeViewModel.list.observe(this) {
        Log.d("HomeViewModel", "observe size:${it.size}")
    }
    homeViewModel.loadData()
}

简单的创建一个 ViewModel ,observe 列表数据对应的 LiveData。
通过输出的日志发现,触发数据加载之后,每次子 Flow 流生产数据的时候,都会触发一次 combine 操作,生成新的数据。

日志输出:
combine  bannerData:null,channelData:null,listData:null
collect: 0
observe size:0

getBannerData success
combine  bannerData:Banner,channelData:null,listData:null
collect: 1
observe size:1

getChannelData success
combine  bannerData:Banner,channelData:Channel,listData:null
collect: 2
observe size:2

getListData success
combine  bannerData:Banner,channelData:Channel,listData:List
collect: 3
observe size:3

总结

具体场景,具体分析。刚好这几个场景,配合Flow进行使用,整体实现也相对简单了一些。

标签:总结,fetchDataFromNetWork,Kotlin,flow,data,collect,加载,数据,CResult
From: https://blog.51cto.com/u_16163453/6534798

相关文章

  • 蔬菜识别系统Python+TensorFlow+Django+卷积神经网络算法
    一、介绍蔬菜识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对数据集进行训练,最后得到一个识别精度较高的模型。并基于Django框架,开发网页端操作平台,实现用户上传一张图片识别其名称。二、效果图片三、演示视频+代码视频+完整......
  • 鸟类识别系统Python+Django+TensorFlow+卷积神经网络算法【完整代码】
    一、介绍鸟类识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对数据集进行训练,最后得到一个识别精度较高的模型。并基于Django框架,开发网页端操作平台,实现用户上传一张图片识别其名称。数据集选自加州理工学院200种鸟类数据集二、......
  • Kotlin协程:Flow基础原理
    本文分析示例代码如下:launch(Dispatchers.Main){flow{emit(1)emit(2)}.collect{delay(1000)withContext(Dispatchers.IO){Log.d("liduo","$it")}Log.d("liduo",&......
  • Kotlin 增量编译是怎么实现的?
    前言编译运行是一个Android开发者每天都要做的工作,增量编译对于开发者也极其重要,高命中率的增量编译可以极大的提高开发者的开发效率与体验之前写了一些文章介绍Kotlin增量编译的原理,以及Kotlin1.7支持了跨模块增量编译了解了这些基本原理之后,我们今天一起来看下Kotlin增量编译......
  • 蔬菜识别系统Python+TensorFlow+Django+卷积神经网络算法
    一、介绍蔬菜识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对数据集进行训练,最后得到一个识别精度较高的模型。并基于Django框架,开发网页端操作平台,实现用户上传一张图片识别其名称。二、效果图片三、演示视频+代码视频+完整代码:http......
  • Android面试技巧总结,这下offer稳啦
    最近有很多朋友给我后台留言:自己投了不少简历,但是收到的面试邀请却特别少;好不容易收到了大厂的面试邀请,但是面试官问得太深了,结果也挂了;对于面试官的问题,明明知道该怎么做,但是却说不清楚。这些问题不是个例,很多人都有这样的困扰。很大一部分是技术层面的问题。薪资比较高的前端岗位......
  • 换个姿势,十分钟拿下Java/Kotlin泛型
    0x1、引言解完BUG,又有时间摸鱼学点东西了,最近在复习Kotlin,跟着朱涛的《Kotlin编程第一课》查缺补漏。看到泛型这一章时,想起之前面一家小公司时的面试题:说下你对泛型协变和逆变的理解?读者可以试试在不查资料的情况下能否答得上来?反正我当时是没想起来,尽管写过一篇《Kotlin刨根问底......
  • Android面试题:Handler、Binder、AMS、WMS面试必问题(带答案,万字总结,精心打磨,快收藏)
    前言业内一直有一个说法:技术好的未必底层够硬,但底层扎实的学起来进步如飞。这也是为什么如今的大厂都非常注重对底层原理的考察:除了可以看出面试者的即战力,底层原理更能看出一个开发者的发展潜力。大家对此应该也深有感受,在面试的过程中,底层原理是无论如何都躲不过去的一关。最典型......
  • 掌控习惯-学习总结
    一直以来,我都有一个困扰自己许久的问题,如何让自己的良好行为形成习惯,比如练字,比如健身,比如读书。这些行为一般会在我制定计划的前几天或者前几周可以顺利实行,但不幸的是,一但因为某天的突发状况将这个过程打断之后,这些行为就像从来没有出现过一样,从我的工作生活中消失得干干净净。......
  • 近期高性能计算报班培训学习总结
    工作以来,我鲜有时间停下来做一些总结,偶有所得也是记录在公司内部的文档里。我细细想来,发现原因主要是这份工作实在是有些螺丝钉了,脱离了公司的环境就很难成立。去年10月底以来,我开始报班培训C++,为什么要这样做呢?因为我发现大厂的算法工程师很容易变成“螺丝钉”,数据、训练和部署......