Kotlin-Flow操作符

151 阅读6分钟

简单的Flow

fun main() {
    runBlocking {
        log("runBlocking")
        // 构造flow
        val flow = flow {
            log("emit")
            // 上游
            emit("emit")
        }
        flow.collect{
            // 下游
            log("collect")
        }
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] runBlocking
[Thread[Test worker @coroutine#1,5,main]] emit
[Thread[Test worker @coroutine#1,5,main]] collect

构造Flow对象,Flow里成员变量为闭包对象(FlowCollector扩展函数)。调用flow的成员变量传入参数为collect的闭包对象(封装了)。

flow调用流程:两个操作符+两个闭包+emit函数。

  1. collect操作符触发调用,执行力flow的闭包
  2. flow闭包里调用emit函数,执行了collect闭包

Flow返回集合

把收集到的数据放到list里。

fun main() {
    runBlocking {
        val result = mutableListOf<String>()
        val flow = flow { 
            emit("emit")
        }
        flow.collect{
            result.add(it)
        }
    }
}

封装收集:

// 这个其实是官方提供的
public suspend fun <T, C : MutableCollection<in T>> Flow<T>.toCollection(destination: C): C {
    collect { value ->
        destination.add(value)
    }
    return destination
}

fun main() {
    runBlocking {
        val result = mutableListOf<String>()
        flow {
            emit("emit")
        }.toList(result)
    }
}

作为Flow的扩展函数。重写了Flow的collect闭包,也就是FlowCollector的emit函数。

Flow变换操作符

transform操作符

fun main() {
    runBlocking {
        val result = mutableListOf<String>()
        flow {
            emit("emit")
        }.transform {
            emit("$it transform")
        }.collect{
            log("collect:$it")
        }
    }
}
public inline fun <T, R> Flow<T>.transform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = flow { // Note: safe flow is used here, because collector is exposed to transform on each operation
    collect { value ->
        // kludge, without it Unit will be returned and TCE won't kick in, KT-28938
        // 上游的数据先经过transform处理
        return@collect transform(value)
    }
}
  1. Flow扩展函数,返回一个新的Flow对象
  2. 新flow对象重写了flow闭包,该闭包调用collect收集了原始Flow的数据
  3. 经过transform处理,我们再次发射处理过的数据
  4. 返回的flow的collect闭包被调用

每调用1个transform操作符就会生成一个Flow对象,该对象装饰了它上一个(扩展)对象,flow1装饰flow,flow2装饰flow1。装饰者模式。

fun main() {
    runBlocking {
        val result = mutableListOf<String>()
        flow {
            emit("emit")
        }.transform {
            emit("$it transform1")
        }.transform {
            emit("$it transform2")
        }.collect{
            log("collect:$it")
        }
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] collect:emit transform1 transform2

transform还需要自己发射数据,map可自动发射(map内部封装了transform)

fun main() {
    runBlocking {
        val result = mutableListOf<String>()
        flow {
            emit("emit")
        }.map {
            "$it map"
        }.collect{
            log("collect:$it")
        }
    }
}

过滤操作符

fun main() {
    runBlocking {
        flow {
            emit("emit1")
            emit("emit2")
        }.filter {
            // 包含emit2字符串才能继续往下发送
            it.contains("emit2")
        }.collect{
            log("collect:$it")
        }
    }
}

Flow操作符多协程原理以及场景

场景:在主线程执行collect操作,在flow闭包里执行耗时操作

fun main() {
    runBlocking {
        flow {
            // flowOn(Dispatchers.IO)使flow闭包(上游)在子线程执行
            log("emit")
            emit("emit")
        }.flowOn(Dispatchers.IO)
            .collect {
                log("collect:$it")
            }
    }
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]] emit
[Thread[Test worker @coroutine#1,5,main]] collect:emit

flow闭包(上游),collect闭包(下游)分别执行在不同的协程以及不同的线程里。

构造了新的协程执行flow闭包,指定了协程分发器为Dispatchers.IO,子线程里执行flow闭包,原理基于ChannelFlow.

Flow处理背压

当上游速度高于下游:

fun main() {
    runBlocking {
        val time = measureTime {
            flow {
                log("emit")
                emit("emit")
                delay(1000)
                emit("emit2")
            }.collect{
                delay(2000)
                log("collect $it")
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] emit
[Thread[Test worker @coroutine#1,5,main]] collect emit
[Thread[Test worker @coroutine#1,5,main]] collect emit2
[Thread[Test worker @coroutine#1,5,main]] time:5.053986s

使用buffer解决背压:

fun main() {
    runBlocking {
        val time = measureTime {
            flow {
                log("emit")
                emit("emit")
                delay(1000)
                emit("emit2")
            }.buffer().collect{
                delay(2000)
                log("collect $it")
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#2,5,main]] emit
[Thread[Test worker @coroutine#1,5,main]] collect emit
[Thread[Test worker @coroutine#1,5,main]] collect emit2
[Thread[Test worker @coroutine#1,5,main]] time:4.056571500s

总时间减少了。

构造新的协程执行flow闭包,上游数据会发送到Channel缓冲区,发送完成继续发送下一条collect操作符监听缓冲区是否由数据,若有则收集成功。原理基于ChannelFlow。

上游覆盖旧数据

场景:上游生产速度很快,下游消费速度慢,我们只关心最新数据,旧的数据可以丢掉。使用conflate操作符:

fun main() {
    runBlocking {
        flow {
            repeat(5){
                log("emit$it")
                emit("emit$it")
                delay(100)
            }
        }.conflate().collect{
            delay(500)
            log("collect $it")
        }
    }
}
输出:
[Thread[Test worker @coroutine#2,5,main]] emit0
[Thread[Test worker @coroutine#2,5,main]] emit1
[Thread[Test worker @coroutine#2,5,main]] emit2
[Thread[Test worker @coroutine#2,5,main]] emit3
[Thread[Test worker @coroutine#2,5,main]] emit4
[Thread[Test worker @coroutine#1,5,main]] collect emit0
[Thread[Test worker @coroutine#1,5,main]] collect emit4

中间数据由于下游没有来的及消费,被上游新的数据冲刷掉了

相当于使用了buffer操作符,该buffer只能容纳一个数据,新来的数据将会覆盖旧的数据。原理基于ChannelFlow

Flow变换取最新值

场景:使用transform处理数据,若它处理数据慢,新的数据过来后取消未处理好的值。 使用transformLatest操作符。

fun main() {
    runBlocking {
        flow {
            repeat(5){
                // 上游协程1
                log("emit$it")
                emit("emit$it")
            }
        }.transformLatest {
            delay(200)
            // 中间协程2
            log("transformLatest $it")
            emit("transformLatest $it")
        }.collect{
            // 下游协程3
            log("collect $it")
        }
    }
}
协程:
[Thread[Test worker @coroutine#2,5,main]] emit0
[Thread[Test worker @coroutine#2,5,main]] emit1
[Thread[Test worker @coroutine#2,5,main]] emit2
[Thread[Test worker @coroutine#2,5,main]] emit3
[Thread[Test worker @coroutine#2,5,main]] emit4
[Thread[Test worker @coroutine#7,5,main]] transformLatest emit4
[Thread[Test worker @coroutine#1,5,main]] collect transformLatest emit4

由于transform处理速度比较慢,上游有新数据过来后会取消transform未处理的数据

override suspend fun flowCollect(collector: FlowCollector<R>) {
    coroutineScope {
        var previousFlow: Job? = null
        //开始收集上游数据
        flow.collect { value ->
            previousFlow?.apply {
                //若是之前的协程还在,则取消
                cancel(ChildCancelledException())
                join()
            }
            //开启协程执行,此处选择不分发新线程
            previousFlow = launch(start = CoroutineStart.UNDISPATCHED) {
                collector.transform(value)
            }
        }
    }
}

构造新的协程执行flow闭包,收集到数据后再开启协程3,在协程里会调用transformLatest的闭包,最终调用collect闭包。协程1继续发送数据,若发现协程2还在运行,则取消协程2。原理基于ChannelFlow

map也有类似操作。

fun main() {
    runBlocking {
        flow {
            repeat(5){
                log("emit$it")
                emit("emit$it")
            }
        }.mapLatest {
            delay(200)
            // 中间协程2
            log("transformLatest $it")
            "transformLatest $it"
        }.collect{
            // 下游协程3
            log("collect $it")
        }
    }
}
输出:
[Thread[Test worker @coroutine#2,5,main]] emit0
[Thread[Test worker @coroutine#2,5,main]] emit1
[Thread[Test worker @coroutine#2,5,main]] emit2
[Thread[Test worker @coroutine#2,5,main]] emit3
[Thread[Test worker @coroutine#2,5,main]] emit4
[Thread[Test worker @coroutine#7,5,main]] transformLatest emit4
[Thread[Test worker @coroutine#1,5,main]] collect transformLatest emit4

收集最新数据

场景:监听下载进度,UI显示最新进度。没必要频繁刷新UI,可以忽略旧数据。 使用collectLatest操作符:

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                repeat(100) {
                    emit(it)
                }
            }
            flow1.collectLatest {
                delay(20)
                log("collectLatest:$it")
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#102,5,main]] collectLatest:99
[Thread[Test worker @coroutine#1,5,main]] time:120.534600ms

开启新的协程flow闭包,若collect收集比较慢,下个数据emit过来会取消未处理的数据。原理基于ChannelFlow

多Flow操作符里的多协程原理以及使用场景

展平流

flatMapConcat

场景:

  1. 请求学生信息,使用flow1
  2. 请求该学生班主任信息,用flow2
  3. 先拿到学生信息,通过信息里带的班主任id取拿班主任信息

串行有前后依关系。使用flatMapConcat操作符:

fun main() {
    runBlocking {
        val flow1 = flow {
            emit("stuInfo")
        }
        flow1.flatMapConcat {
            // flow2
            flow {
                emit("$it teachInfo")
            }
        }.collect{
            log("collect:$it")
        }
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo teachInfo

展平,将两个flow数据拍平了输出

可以请求多个学生班主任信息:

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                log("flow1 emit")
                emit("stuInfo 1")
                emit("stuInfo 2")
                emit("stuInfo 3")
            }
            flow1.flatMapConcat {
                // flow2
                flow {
                    log("flatMapConcat: $it teachInfo")
                    emit("$it teachInfo")
                    delay(1000)
                }
            }.collect {
                log("collect:$it")
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] flow1 emit
[Thread[Test worker @coroutine#1,5,main]] flatMapConcat: stuInfo 1 teachInfo
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 1 teachInfo
[Thread[Test worker @coroutine#1,5,main]] flatMapConcat: stuInfo 2 teachInfo
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 2 teachInfo
[Thread[Test worker @coroutine#1,5,main]] flatMapConcat: stuInfo 3 teachInfo
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 3 teachInfo
[Thread[Test worker @coroutine#1,5,main]] time:3032

不涉及多协程,使用装饰模式将flow2使用map变换,将flow1、flow2数据发射出来,Concat将两个flow链接起来。

flatMapMerge

场景:同时查看多个学生班主任信息

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                log("flow1 emit")
                emit("stuInfo 1")
                emit("stuInfo 2")
                emit("stuInfo 3")
            }
            flow1.flatMapMerge(4) {
                // flow2
                flow {
                    log("flatMapMerge: $it teachInfo")
                    emit("$it teachInfo")
                    delay(1000)
                }
            }.collect {
                log("collect:$it")
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#2,5,main]] flow1 emit
[Thread[Test worker @coroutine#3,5,main]] flatMapMerge: stuInfo 1 teachInfo
[Thread[Test worker @coroutine#4,5,main]] flatMapMerge: stuInfo 2 teachInfo
[Thread[Test worker @coroutine#5,5,main]] flatMapMerge: stuInfo 3 teachInfo
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 1 teachInfo
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 2 teachInfo
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 3 teachInfo
[Thread[Test worker @coroutine#1,5,main]] time:1.073181s

flatMapMerge是并发执行,速度比flatMapConcat快。flatMapMerge可以指定并发数量,指定flatMapMerge(0)。flatMapMerge退化为flatMapConcat

override suspend fun collectTo(scope: ProducerScope<T>) {
    val semaphore = Semaphore(concurrency)
    val collector = SendingCollector(scope)
    val job: Job? = coroutineContext[Job]
    flow.collect { inner ->
        job?.ensureActive()
        //并发数限制锁
        semaphore.acquire()
        scope.launch {
            //开启新的协程
            try {
                //执行flatMapMerge闭包里的flow
                inner.collect(collector)
            } finally {
                semaphore.release() // Release concurrency permit
            }
        }
    }
}

flow1里的每个学生会触发班主任信息flow2。新来了协程去执行flow2里的闭包。原理基于ChannelFlow

flatMpaLatest

场景:flatMapConcat是线性执行的,可以使用flatMapMerge提升效率。为了节省资源,请求班主任信息时,若某个学生班主任信息没有返回,而下一个学生班主任信息已经开始请求,则取消上一个没有返回的班主任Flow。使用flatMapLatest

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                emit("stuInfo 1")
                emit("stuInfo 2")
                emit("stuInfo 3")
            }
            flow1.flatMapLatest {
                // flow2
                flow {
                    delay(1000)
                    emit("$it teachInfo")
                }
            }.collect {
                log("collect:$it")
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] collect:stuInfo 3 teachInfo
[Thread[Test worker @coroutine#1,5,main]] time:1.082095300s

和transformLatest相似。原理基于ChannelFlow。 transformLatest、mapLatest、collectLatest、flapMapLatest核心实现都是ChannelFlowTransformLatest,最终继承自:ChannelFlow

组合流

combine。场景:查询学生的性别以及选修了某个课程。 查询性别与选修课程可以同时发出请求:

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                emit("stuSex 1")
                emit("stuSex 2")
                emit("stuSex 3")
            }
            val flow2 = flow {
                emit("stuSubject")
            }
            flow1.combine(flow2){
                sex,subject->"sex:$sex subject:$subject"
            }.collect{
                log(it)
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] sex:stuSex 1 subject:stuSubject
[Thread[Test worker @coroutine#1,5,main]] sex:stuSex 2 subject:stuSubject
[Thread[Test worker @coroutine#1,5,main]] sex:stuSex 3 subject:stuSubject
[Thread[Test worker @coroutine#1,5,main]] time:83.032900ms

flow1的每个emit和flow2的emit关联了起来。 combine特点:短的一方会等待长的一方结束后才结束。

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                emit("a")
                emit("b")
                emit("c")
                emit("d")
            }
            val flow2 = flow {
                emit("1")
                emit("2")
            }
            flow1.combine(flow2){
                sex,subject->"sex:$sex subject:$subject"
            }.collect{
                log(it)
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] sex:a subject:1
[Thread[Test worker @coroutine#1,5,main]] sex:b subject:2
[Thread[Test worker @coroutine#1,5,main]] sex:c subject:2
[Thread[Test worker @coroutine#1,5,main]] sex:d subject:2
[Thread[Test worker @coroutine#1,5,main]] time:49.213100ms

flow2早就发射了2,会一直等到flow1发射结束。

zip

在combine基础上,无论是学生性别还是学生课程,只要某个flow结束了就取消flow。 使用zip操作符:

fun main() {
    runBlocking {
        val time = measureTime {
            val flow1 = flow {
                emit("a")
                emit("b")
                emit("c")
                emit("d")
            }
            val flow2 = flow {
                emit("1")
                emit("2")
            }
            flow1.zip(flow2){
                sex,subject->"sex:$sex subject:$subject"
            }.collect{
                log(it)
            }
        }
        log("time:$time")
    }
}
输出:
[Thread[Test worker @coroutine#1,5,main]] sex:a subject:1
[Thread[Test worker @coroutine#1,5,main]] sex:b subject:2
[Thread[Test worker @coroutine#1,5,main]] time:36.900100ms

flow2结束了,flow1没发送完成。

短的flow结束,另一个flow也结束。

Flow操作符

fiter map flowOn buffer callbackFlow flatMapxx以及comnine zip