Kotlin 协程Flow VS Rxjava2 (一) 协程 Flow

5,497 阅读5分钟

Flow API是什么

Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库。是 Kotlin 协程与响应式编程模型结合的产物,与 RxJava 非常相似。Flow结合协程可以代替RxjavaAndroid中的地位

RxJava中一样,Flow在订阅者进行订阅之前,其主体不会被执行。即在流生成器内部的代码到了收集流后才开始运行。

创建Flow

元素的发送通过emit函数提供

        val flow = flow {
            (1..10).forEach {
                emit(1)
                delay(200)
            }
        }.flowon(Dispatchers.IO)

Flow内部通过emit方法来发送数据,类似于RxjavaonNext()。另外执行体内部还可以调用其他的挂起函数。

flowon(Dispatchers.IO)指定运行时所在的调度器为IO调度器,对它之前的操作有影响,类似于RxJavasubscribeOn,消费者的线程就是调度协程的线程。

在 RxJava 的学习和使用过程中, subscribeOnobserveOn 经常容易被混淆;而在 Flow 当中 collect 所在的协程自然就是观察者,它想运行在什么调度器上它自己指定即可,非常容易区分。

消费Flow

通过collect()来消费Flow类似于Rxjavasubscribe()。因为collect()是一个挂起函数,所以需要在挂起函数或者协程中调用,消费者的所在的线程由消费者协程的调度器决定

    fun flowDemo2() {
        val flow = flow {
            Log.i(TAG, "flow")
            (1..10).forEach {
                emit(1)
                delay(200)
            }
        }.flowOn(Dispatchers.IO)
        viewModelScope.launch {
            flow.collect { Log.i(TAG, it.toString()) }
        }
    }

冷数据流

所谓数据流,就是只有消费时才会生产的数据流。一个 Flow 创建出来之后,不消费则不生产,多次消费则多次生产,生产和消费总是相对应的。RxJavaObservable 也是如此,每次调用它的 subscribe 都会重新消费一次。

Flow就是冷数据流,这一点与 Channel 正对应:Channel 的发送端并不依赖于接收端。

异常的处理

直接调用catch函数捕获异常

        val flow = flow {
            (1..10).forEach {
                emit(1)
                delay(200)
            }
        }.catch {
            Log.i(TAG, it.message)
            // 实现Rxjava的onErrorReturn
            emit(10)
        }.onCompletion {
            // 相当于finally 无论前面是否存在异常,它都会被调用 
            Log.i(TAG, "onCompletion")
        }

onCompletion类似于try catch中的finally

末端操作符

除了collect直接消费事件的末端操作符,asLiveData()可以直接将一个flow流转化为一个LiveData对象

liveData中可以通过emitSource来发送

    val currentName = liveData(Dispatchers.IO) {
        try {
            Log.i(TAG, "发送者的线程:" + Thread.currentThread().name)
            emitSource(flowDemo())
        } catch (e: Throwable) {
            e.printStackTrace()
        }
    }
    
    suspend fun flowDemo(): LiveData<Int> {
        return flow {
            List(100) {
                emit(it)
            }
        }.asLiveData()
    }

取消Flow

flow没有取消的方法,直接调用消费者协程的cancel方法即可取消flow

其他Flow的创建方式

listOf(1, 2, 3, 4).asFlow()
setOf(1, 2, 3, 4).asFlow()
flowOf(1, 2, 3, 4)

channelFlow()

    channelFlow {
        for (i in 1..5) {
            delay(100)
            send(i)
        }
    }.collect{
        println(it)
    }

flow 是"冷流"(Cold Stream)。在没有切换线程的情况下,生产者和消费者是同步非阻塞的。在切换线程的情况下跟channelFlow是一致

channelFlowHot Stream 实现了生产者和消费者异步非阻塞模型。


Flow的其他常用操作符

map:元素变换,也可以映射成其他Flow。集合了RxjavamapflatMap的功能

        val flow = flow {
            emit(1)
        }.map {
            repository.sendNetworkRequestSuspend()
        }

retry: 有异常的情况下重试

  // 5秒轮询一次 错误重试三次    
  suspend fun flowDemo(): LiveData<String> {
        return flow {
            while (true) {
                emit(repository.sendNetworkRequestSuspend())
                delay(5000)
            }
        }.map {
            it.html_url
        }.retry(3).catch {
            // 类似于RxJava的onError
            Log.e(TAG, it.message)
        }.onCompletion {
            // 类似于Rxjava中的onComplete
            Log.i(TAG, "finally")
        }.flowOn(Dispatchers.IO).asLiveData()
    }
  
 val currentName = liveData {
        try {
            emitSource(flowDemo())
        } catch (e: Throwable) {
            e.printStackTrace()
        }
    }

Retrywhen:满足条件为true时重试

zip:合并两个flow数据流,会分别对两个流合并处理,也就是快的流要等慢的流发射完才能合并。一般用作合并两个网络请求返回数据

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms
val startTime = System.currentTimeMillis() // remember the start time 
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string with "zip"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

//1 -> one at 471 ms from start
//2 -> two at 870 ms from start
//3 -> three at 1271 ms from start

combine:使用 combine 合并时,每次从 flowA 发出新的 item ,会将其与 flowB 的最新的 item 合并。

fun main() = runBlocking {
    val flowA = (1..5).asFlow().onEach { delay(100)  }
    val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200)  }
    flowA.combine(flowB) { a, b -> "$a and $b" }
        .collect { println(it) }
}

// 1 and one
// 2 and one
// 3 and one
// 3 and two
// 4 and two
// 5 and two
// 5 and three
// 5 and four
// 5 and five

flattenMerge : flowA、flowB 作为单个流的执行。类似于Rxjava的的merge

    val flowA = (1..5).asFlow().onEach { delay(100) }
    val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }

    flowOf(flowA,flowB)
        .flattenMerge(2)
        .collect{ println(it) }

filter:过滤

take:获取集合数据的前几个数据

drop:过滤集合的前集几个数据

 suspend fun flowDemo(): LiveData<Int> {
        return flow {
            (1..10).forEach {
                emit(it)
            }
        }.take(3).asLiveData()
    }

onEach:遍历

onStart:开始会执行


协程背压

Kotlin协程支持背压。Kotlin流程设计中的所有函数都标有suspend修饰符-具有在不阻塞线程的情况下挂起调用程序执行的强大功能。因此,当流的收集器不堪重负时,它可以简单地挂起发射器,并在准备好接受更多元素时稍后将其恢复。

buffer() 对应RxJava中的 BUFFER 策略

没有固定大小,可以无限制添加数据,不会抛出 MissingBackpressureException 异常,但可能会导致 OOM

fun main() = runBlocking {
    var start = 0L
    val time = measureTimeMillis {
        (1..5)
                .asFlow()
                .onStart { start = System.currentTimeMillis() }
                .onEach {
                    delay(100)
                    println("Emit $it (${System.currentTimeMillis() - start}ms) ")
                }
                .buffer()
                .flowOn(Dispatchers.IO)
                .collect {
                    println("Collect $it starts (${System.currentTimeMillis() - start}ms) ")
                    delay(500)
                    println("Collect $it ends (${System.currentTimeMillis() - start}ms) ")
                }
    }

    println("Cost $time ms")
}

Emit 1 (109ms) 
Collect 1 starts (115ms) 
Emit 2 (219ms) 
Emit 3 (324ms) 
Emit 4 (426ms) 
Emit 5 (531ms) 
Collect 1 ends (618ms) 
Collect 2 starts (618ms) 
Collect 2 ends (1122ms) 
Collect 3 starts (1123ms) 
Collect 3 ends (1625ms) 
Collect 4 starts (1625ms) 
Collect 4 ends (2127ms) 
Collect 5 starts (2127ms) 
Collect 5 ends (2627ms) 
Cost 2683 ms

conflate() 对应 LATEST 策略

如果缓存池满了,新数据会覆盖老数据

将上面buffer()改成conflate()接口如下

Emit 1 (114ms) 
Collect 1 starts (117ms) 
Emit 2 (217ms) 
Emit 3 (329ms) 
Emit 4 (433ms) 
Emit 5 (538ms) 
Collect 1 ends (620ms) 
Collect 5 starts (620ms) 
Collect 5 ends (1124ms) 
Cost 1171 ms

collectLatest()

只处理最新的数据,这看上去似乎与 conflate 没有区别,其实区别大了:它并不会直接用新数据覆盖老数据,而是每一个都会被处理,只不过如果前一个还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。

flow {
  List(100) {
    emit(it)
  }
}.collectLatest { value ->
  println("Collecting $value")
  delay(100)
  println("$value collected")
}

结果如下:

Collecting 0
Collecting 1
...
Collecting 97
Collecting 98
Collecting 99100ms later
99 collected