kotlin flow如何结合Android使用场景

目录

背景

flow 介绍

flow加载列表数据

总结


背景

        flow简单的可以理解为数据流,它可以生成连续的同类型数据。刚接触到flow的开发者都很疑惑,它的功能好像都有东西可以替代。比如通过foreach遍历Collection或Sequence都能有flow一样的生成数据效果,那为什么还要引入flow呢。大家可能会认为flow实现了观察者模式,这点与collection或sequence的遍历不同。其实LiveData就是按照观察者模式设计的,LiveData配合集合的遍历就可以达到数据被观察的目的。

        刚接触flow时想理解它的本质目的确实有点费劲,但是经过简单的实践后我们发现他的优势表现在与协程的配合上。大家想一想Collection或是sequence的操作支持挂起吗?答案是否定的,它们不支持。但是flow的操作都是挂起函数,用户可以在flow的不同操作中调用其他的挂起函数,并且flow还可以通过flowon来切换flow所运行的协程。

flow 介绍

flow特质:

  1. 在协程中与产生一条数据的挂起函数比,flow可以有序生成多条数据。
  2. 与生成多条数据的Iterator相比,flow在数据生成的过程中可以调用挂起函数异步生成数据,同时不会阻塞当前线程。
  3. 生成的数据序列是同类型的数据。

flow中的三个角色:

  1. 数据的生成者=》可以通过挂起函数异步生产一系列数据。
  2. 中介者=》可以对生成的数据进行修改。
  3. 数据的消费者=》使用生成的数据,一般用户界面展示。
        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的方式加载列表数据时有下面几个特点:

  1. flow的链式调用替代了异步回调的方式,代码简洁易懂,避免了异步回调反复嵌套的问题。
  2. 使用flowOn方法可以方便灵活地进行线程切换,并且在flow操作中都支持挂起方法,flow可以无缝对接协程。
  3. flow处理过程是声明式的,只有flow被收集的时候这些声明的过程才被执行。声明式的过程还有个好处是我们可以基于已有的flow声明再追加新的处理过程声明。

        这篇文章以最简单的方式展示了flow加载列表数据的流程,在实际应用中肯定要更复杂些。这里的flow声明都在fragment中,实际应用中还要进行基本的分层处理。flow的声明属于DataSource层的。在flow向上传递的过程中,我们可以为底层的flow声明新的处理,比如在repository层追加声明本地缓存处理,在viewmodel层追加声明ui状态更新处理等。本质就是将例子中的处理分解到不同层次上进行追加声明。

        我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号

kotlin flow如何结合Android使用场景

上一篇:Azkaban 自定义邮件内容以及格式 源码修改


下一篇:P3128 [USACO15DEC]Max Flow P ( 树上差分 )