目录
背景
flow简单的可以理解为数据流,它可以生成连续的同类型数据。刚接触到flow的开发者都很疑惑,它的功能好像都有东西可以替代。比如通过foreach遍历Collection或Sequence都能有flow一样的生成数据效果,那为什么还要引入flow呢。大家可能会认为flow实现了观察者模式,这点与collection或sequence的遍历不同。其实LiveData就是按照观察者模式设计的,LiveData配合集合的遍历就可以达到数据被观察的目的。
刚接触flow时想理解它的本质目的确实有点费劲,但是经过简单的实践后我们发现他的优势表现在与协程的配合上。大家想一想Collection或是sequence的操作支持挂起吗?答案是否定的,它们不支持。但是flow的操作都是挂起函数,用户可以在flow的不同操作中调用其他的挂起函数,并且flow还可以通过flowon来切换flow所运行的协程。
flow 介绍
flow特质:
- 在协程中与产生一条数据的挂起函数比,flow可以有序生成多条数据。
- 与生成多条数据的Iterator相比,flow在数据生成的过程中可以调用挂起函数异步生成数据,同时不会阻塞当前线程。
- 生成的数据序列是同类型的数据。
flow中的三个角色:
- 数据的生成者=》可以通过挂起函数异步生产一系列数据。
- 中介者=》可以对生成的数据进行修改。
- 数据的消费者=》使用生成的数据,一般用户界面展示。
flow{// 生成1,2,3数据序列
emit(1)
emit(2)
emit(3)
}.map {
value -> value * 2 //修改数据
}.collect {
result-> println(result) //显示转换过的数据
}
flow加载列表数据
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的链式调用替代了异步回调的方式,代码简洁易懂,避免了异步回调反复嵌套的问题。
- 使用flowOn方法可以方便灵活地进行线程切换,并且在flow操作中都支持挂起方法,flow可以无缝对接协程。
- flow处理过程是声明式的,只有flow被收集的时候这些声明的过程才被执行。声明式的过程还有个好处是我们可以基于已有的flow声明再追加新的处理过程声明。
这篇文章以最简单的方式展示了flow加载列表数据的流程,在实际应用中肯定要更复杂些。这里的flow声明都在fragment中,实际应用中还要进行基本的分层处理。flow的声明属于DataSource层的。在flow向上传递的过程中,我们可以为底层的flow声明新的处理,比如在repository层追加声明本地缓存处理,在viewmodel层追加声明ui状态更新处理等。本质就是将例子中的处理分解到不同层次上进行追加声明。
我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号