目录
背景
flow 介绍
flow加载列表数据
总结
flow简单的可以理解为数据流,它可以生成连续的同类型数据。刚接触到flow的开发者都很疑惑,它的功能好像都有东西可以替代。比如通过foreach遍历Collection或Sequence都能有flow一样的生成数据效果,那为什么还要引入flow呢。大家可能会认为flow实现了观察者模式,这点与collection或sequence的遍历不同。其实LiveData就是按照观察者模式设计的,LiveData配合集合的遍历就可以达到数据被观察的目的。
刚接触flow时想理解它的本质目的确实有点费劲,但是经过简单的实践后我们发现他的优势表现在与协程的配合上。大家想一想Collection或是sequence的操作支持挂起吗?答案是否定的,它们不支持。但是flow的操作都是挂起函数,用户可以在flow的不同操作中调用其他的挂起函数,并且flow还可以通过flowon来切换flow所运行的协程。
flow特质:
flow中的三个角色:
flow{// 生成1,2,3数据序列 emit(1) emit(2) emit(3) }.map { value -> value * 2 //修改数据 }.collect { result-> println(result) //显示转换过的数据 }
Android应用加载列表数据是一个比较普遍的需求,我们如何使用flow实现列表数据的加载和显示呢?
首先我们先分析下再加载列表数据都需要处理哪些问题:
1. 加载数据过程中显示loading,数据加载完成隐藏loading。
flow { val ret = serverApi.getList(requestId) emit(ret) }.onStart { progressBar.visibility = View.VISIBLE }.onCompletion { progressBar.visibility = View.INVISIBLE }.collect()
onStart在数据流开始收集的时候被调用,onCompletion在数据流结束时被调用。这里面数据是通过挂起方法getList生成的单一数据,所以这个数据流生成一条数据后就结束了。我们可以发现这里通过数据流的链式处理再配合协程的挂起函数,我们可以避免异步回调的使用。
2.当加载的数据为空时显示空画面。
flow { val ret = serverApi.getList(requestId) if (ret.isNotEmpty()) { emit(ret) } }.onStart { progressBar.visibility = View.VISIBLE }.onEmpty { loadDataRetryButton.visibility = View.VISIBLE }.onCompletion { progressBar.visibility = View.INVISIBLE }.collect()
onEmpty在数据为空时被调用,那什么情况是数据为空呢?其实数据流的数据为空只的是数据流被收集时,数据流没有生成任何数据,在这里就是没有调用emit发射任何数据的时候。我们可以看到ret.isNotEmpty的判断,只有数据不为空时才进行发射,数据为空时没有发射任何数据,这时onEmpty被调用。
3.获取数据过程中发送异常时,我们需要显示异常画面。
flow { val ret = serverApi.getList(requestId) if (ret.isNotEmpty()) { emit(ret) } }.onStart { progressBar.visibility = View.VISIBLE }.onEmpty { loadDataRetryButton.visibility = View.VISIBLE }.catch { msgTextView.visibility = View.VISIBLE msgTextView.text = "发送异常" loadDataRetryButton.visibility = View.VISIBLE }.onCompletion { progressBar.visibility = View.INVISIBLE }.collect()
catch在数据流生成过程中发生异常的时候被调用,我们在catch块中显示错误信息。有一点需要注意,catch块写的位置直接影响了捕获异常的范围。在flow的链式调用中,catch块只会捕获链式调用中它前面的处理产生的异常。
4.显示flow生成的列表数据
flow { val ret = serverApi.getList(requestId) if (ret.isNotEmpty()) { emit(ret) } }.onStart { progressBar.visibility = View.VISIBLE }.onEmpty { loadDataRetryButton.visibility = View.VISIBLE }.onEach { adapter.setData(it) adapter.notifyDataSetChanged() }.catch { msgTextView.visibility = View.VISIBLE msgTextView.text = "发送异常" loadDataRetryButton.visibility = View.VISIBLE }.onCompletion { progressBar.visibility = View.INVISIBLE }.collect{ print(it) }
onEach在每条数据被发射后会被调用,我们可以在这里接收并显示数据。当然我们也可以在collect中显示数据,但是onEach有个优势,它可以写在catch块前面,这样onEach中产生的异常也可以被catch块捕获,collect就没有这样的优势。
5.在网络数据获取失败的情况下使用本地缓存的数据。
flow { val ret = serverApi.getList(requestId) if (ret.isNotEmpty()) { emit(ret) } }.onStart { progressBar.visibility = View.VISIBLE }.onEmpty { loadDataRetryButton.visibility = View.VISIBLE }.catch { if (cacheList.isEmpty()) { msgTextView.text = "发生异常" loadDataRetryButton.visibility = View.VISIBLE } else { emit(cacheList) } }.onEach { cacheList = cacheList adapter.setData(it) adapter.notifyDataSetChanged() }.catch{ msgTextView.text = "onEach异常" loadDataRetryButton.visibility = View.VISIBLE }.onCompletion { progressBar.visibility = View.INVISIBLE }.collect{ print(it) }
在onEach块中我们把成功获取的数据进行保存,然后在catch块中我们判断是否有缓存数据,如果有缓存数据则向下游发射。这里需要注意的是catch块中调用emit发射的数据只能被链式调用的catch块后面的操作接收到。这里大家可能要问,在onEach中发射的异常我们如何捕获?其实在链式操作中,所有的操作都可以使用多次,所以我们可以在onEach块后追加一个catch块来捕获onEach中发生的异常。
6.数据获取和处理的过程中可以方便的切换线程,挂起线程而不是阻塞线程。
var listDataFlow= flow { val ret = serverApi.getList(requestId) if (ret.isNotEmpty()) { emit(ret) } }flowOn(Dispatchers.IO) .onStart { progressBar.visibility = View.VISIBLE }.onEmpty { loadDataRetryButton.visibility = View.VISIBLE }.catch { if (cacheList.isEmpty()) { msgTextView.text = "发生异常" loadDataRetryButton.visibility = View.VISIBLE } else { emit(cacheList) } }.onEach { cacheList = cacheList adapter.setData(it) adapter.notifyDataSetChanged() }.catch{ msgTextView.text = "onEach异常" loadDataRetryButton.visibility = View.VISIBLE }.onCompletion { progressBar.visibility = View.INVISIBLE } lifecycleScope.launch { listDataFlow.collect() }
getList方法是耗时方法,通常需要异步线程配合回调函数来处理。flow支持挂起方法调用,所以这里的getList方式被声明成suspend方法,然后通过flowOn方法切换到IO线程执行getList方法。flowOn只影响链式调用中它前面的方法的执行线程,对后面的方法执行线程没有影响。那么后面的方法执行在哪个线程呢?答案是后面的方法执行在收集方法collect被调用的线程。这里启动协程时没有指定线程,所以它执行在Android的主线程中。
使用flow的方式加载列表数据时有下面几个特点:
这篇文章以最简单的方式展示了flow加载列表数据的流程,在实际应用中肯定要更复杂些。这里的flow声明都在fragment中,实际应用中还要进行基本的分层处理。flow的声明属于DataSource层的。在flow向上传递的过程中,我们可以为底层的flow声明新的处理,比如在repository层追加声明本地缓存处理,在viewmodel层追加声明ui状态更新处理等。本质就是将例子中的处理分解到不同层次上进行追加声明。
我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号