Kotlin Flow :流是连续的也是冷的

4,660 阅读4分钟

前言

Kotlin 协程下 Flow 的推出,让人耳目一新。新鲜的技术总是很诱人,最近搭建一套新的脚手架,用于将来新的应用开发,技术选型上用到了 Flow 。搭建基础框架的过程遇到关于 Flow 使用上的坑,或者说是api使用、链式调用和对扩展函数使用不熟练的坑。另外,谷歌官方也有逐步使用 Flow的迹象。 例如:使用 Paging 3 实现分页加载实战 | 在 Room 中使用 Flow 的demo中使用到了 Flow

关于 Flow 的简单介绍及使用

  • FLow: 异步流。概念上讲依然是响应式流。这和 Rxjava 很像。熟悉 Rxjava 的开发者可以很快适应 Flow。Flow 提供了很多丰富的操作符,例如 mapflitercount 等等,相比 Rxjava ,Flow 的使用和线程切换更为简单。以下是一个简单的例子。

  • 过渡操作符: 过渡操作符应用于上游流,并返回下游流。这些操作符也是冷操作符,也就是说如果没有 '被订阅'就不会执行。onStart/catch/onCompletion/map/filter 等都是过渡操作符。

  • 末端流操作符:在流上用于启动流收集的挂起函数。 collect 是最基础的末端操作符。first/toList/reduce 等都是末端操作符。类型 Rxjava 的订阅。这里可以理解是收集最终的流结果。

  • 冷流:Flow 是一种类似于序列的冷流 — 上游的代码直到流被收集的时候才运行,类似 RxJava 上游流被订阅了才会执行。

  • 使用演示 更加详细的使用可以参考 协程 Flow 最佳实践 | 基于 Android 开发者峰会应用


    private fun getData(): String {
        Thread.sleep(3000)
        return "Flow test"
    }

    @Test
    fun testFlowNormal() {
        GlobalScope.launch {
            flow {
                emit(getData())
            }.flowOn(Dispatchers.IO) //切换线程
                .onStart {
                    println("onStart")
                }.catch {
                    println("catch:${it.message}")//有异常会进入此方法
                }.onCompletion {
                    println("oniComplete:${it?.message}")//无论是否有异常都会执行
                }.collect {
                    println("result = $it")
                }
        }
        Thread.sleep(6000)
    }

输出结果:

onStart
result = Flow test
oniComplete:null

注: catchonCompletion 的执行是有顺序的,顺序为调用先后的顺序。

已上调用的方法先贴出部分方法源码(方法名、参数、返回值类型) ,后面会用到

  1. flowOn: 可以用与上下文切换,常用与线程切换,除 context == EmptyCoroutineContext 外,其他返回值为一个新的 Flow 对象
public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T> {
    checkFlowContext(context)
    return when {
        context == EmptyCoroutineContext -> this
        this is FusibleFlow -> fuse(context = context)
        else -> ChannelFlowOperatorImpl(this, context = context)
    }
}
  1. onStart: 流开始执行,返回值为新的 Flow 对象
public fun <T> Flow<T>.onStart(
    action: suspend FlowCollector<T>.() -> Unit
): Flow<T> = unsafeFlow{...}
  1. catch: 流异常捕获,返回值为新的 Flow 对象
public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
    flow {
        val exception = catchImpl(this)
        if (exception != null) action(exception)
    }
  1. onCompletion: 流执行最终执行结束,放回值为新的 Flow 对象
public fun <T> Flow<T>.onCompletion(
    action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit
): Flow<T> = unsafeFlow {...}
  1. collect: 末端操作符,类型 Rxjava 的订阅
public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
    collect(object : FlowCollector<T> {
        override suspend fun emit(value: T) = action(value)
    })

留意到上面几个方法都是 FLow 的扩展方法,且有几个是特别说明了它们的返回值的。下面就是我就开始了我挖坑出坑之路。

为什么过渡操作符的代码都没有执行到?

根据具体业务模拟出有问题的代码:

    @Test
    fun testFlowSingle() {
        GlobalScope.launch {
            val flow = flow<Any> {
                emit(getData())
            }
            println("flow obj = ${flow.hashCode()}")
            val flowOn = flow.flowOn(Dispatchers.IO)
            println("flowOn obj = ${flowOn.hashCode()}")
            val catch = flow.catch {
                println("catch:${it.message}")
            }
            println("catch obj = ${catch.hashCode()}")
            val onStart = flow.onStart {
                println("onStart")
            }
            println("onStart obj = ${onStart.hashCode()}")
            val onCompletion = flow.onCompletion {
                println("onComplete:${it?.message}")
            }
            println("onCompletion obj = ${onCompletion.hashCode()}")
            flow.collect {
                println("collect result = $it")
            }
            println("last obj = ${flow.hashCode()}")
        }
        Thread.sleep(6000)
    }

输出结果:

flow obj = 1846810080
flowOn obj = 2093635757
catch obj = 438978854
onStart obj = 1552526949
onCompletion obj = 175428485
collect result = Flow test
last obj = 1846810080

如你所看到的 onStart/catch/onCompletion 的方法快里的代码一个都没打印出来。flowOn 也是没有我想过的线程切换效果。顺便也把每个 flow 对象的 hashCode 也打印出来,通过对比后发现:除了末端操作符操作后的对象和开始 flow{...} 创建的对象一致,其他操作符操作过得到的对象都和其他的不一样。

流是连续的也是冷的

在 Kotlin 官方文档上面似乎找到了代码不执行的答案: www.kotlincn.net/docs/refere…

流的每次单独收集都是按顺序执⾏的, 除⾮进⾏特殊操作的操作符使⽤多个流。 该收集过程直接在协程中运⾏, 该协程调⽤末端操作符。 默认情况下不启动新协程。 从上游到下游每个过渡操作符都会处理每个发射出的值然后再交给末端操作符

重点:从上游到下游每个过渡操作符都会处理每个发射出的值然后再交给末端操作符

所以为什么上面的问题 为什么 flowOn/onStart/catch/onCompletion 都没有执行到 就有了答案:上游过渡操作符产生的流没有交给末端操作符。就是这么简单。这也是标题所说的,流是连续的,也是冷的。

最后:把上下游的流接上

已经知道流最终是需要给到末端操作符的,那么,只要把每个过渡操作符发射过的流连接起来就行了。一种是开始的时候使用样例那样以链的形式,第二种就是接收每次过渡操作符的对象。下面来看下第二种:

    @Test
    fun testFlowSingle2() {
        GlobalScope.launch {
            // 每次操作符得到的对象都赋值给 flow 
            var flow = flow<Any> {
                emit(getData())
            }
            println("flow obj = ${flow.hashCode()}")
            flow = flow.flowOn(Dispatchers.IO)
            println("flowOn obj = ${flow.hashCode()}")
            flow = flow.catch {
                println("catch:${it.message}")
            }
            println("catch obj = ${flow.hashCode()}")
            flow = flow.onStart {
                println("onStart")
            }
            println("onStart obj = ${flow.hashCode()}")
            flow = flow.onCompletion {
                println("onComplete:${it?.message}")
            }
            println("onCompletion obj = ${flow.hashCode()}")
            flow.collect {
                println("collect result = $it")
            }
            println("last obj = ${flow.hashCode()}")
        }
        Thread.sleep(6000)
    }

输出结果:

flow obj = 1846810080
flowOn obj = 2093635757
catch obj = 438978854
onStart obj = 1552526949
onCompletion obj = 175428485
onStart
collect result = Flow test
onComplete:null
last obj = 175428485

从结果看,过渡操作符都已经执行。出坑!

有任何技术上的交流,随时欢迎~