Kotlin语言(十三):Flow

注:本文中使用 runBlocking 是为了方便测试,业务开发中禁止使用

一、Flow 的基本使用

1、Sequence 序列生成器

(1)取出序列生成器中的值,需要迭代序列生成器;
(2)是同步调用,是阻塞的,无法调用其它的挂起函数。

fun sequenceFun() {
    val sequence = sequence<Int> {
        Thread.sleep(1000)
        yield(1)
        Thread.sleep(1000)
        yield(2)
        Thread.sleep(1000)
        yield(3)
    }
    sequence.forEach {
        println(it)
    }
    println("Done!")

    // 1
    // 2
    // 3
    // Done!
}

2、Flow 的简单使用

(1)flow{ ... } 内部可以调用 suspend 函数;
(2)使用 emit() 方法来发射数据;
(3)使用 collect() 方法来收集结果。

fun flowFun() = runBlocking {
    val flow = flow {
        delay(1000)
        emit(1)
        delay(1000)
        emit(2)
        delay(1000)
        emit(3)
    }
    flow.collect {
        println(it)
    }
    println("Done!")

    // 1
    // 2
    // 3
    // Done!
}

3、创建 Flow 的常用方式

(1)flow{...} 需要显示调用 emit() 发射数据;
(2)flowOf() 一个发射固定值集的流, 不需要显示调用 emit() 发射数据;
(3)asFlow() 扩展函数,可以将各种集合与序列转换为流,也不需要显示调用 emit() 发射数据。

fun createFlowFun() = runBlocking {
    val flow1 = flow {
        delay(1000)
        emit(1)
    }
    val flow2 = flowOf(2, 3).onEach {
        delay(1000)
    }
    val flow3 = listOf(4, 5).asFlow().onEach {
        delay(1000)
    }
    flow1.collect {
        println(it)
    }
    flow2.collect {
        println(it)
    }
    flow3.collect {
        println(it)
    }
    println("Done!")

    // 1
    // 2
    // 3
    // 4
    // 5
    // Done!
}

4、Flow 是冷流(惰性的)

如同 Sequences 一样, Flow 也是惰性的,即在调用末端流操作符( collect 是其中之一)之前,flow{ ... } 中的代码不会执行。我们称之为 -- 冷流

fun coldFlowFun() = runBlocking {
    val flow = flowOf(1, 2, 3)
        .onEach {
            delay(1000)
        }
    println("calling collect...")
    flow.collect {
        println(it)
    }
    println("calling collect again...")
    flow.collect {
        println(it)
    }

    // calling collect...
    // 1
    // 2
    // 3
    // calling collect again...
    // 1
    // 2
    // 3
}

5、Flow 的取消

流采用了与协程同样的协助取消,取消 Flow 只需要取消它所在的 协程 即可。

fun cancelFlowFun() = runBlocking {
    val flow = flow {
        for (i in 1..3) {
            delay(100)
            println("Emitting $i")
            emit(i)
        }
    }
    withTimeoutOrNull(250) {
        flow.collect {
            println(it)
        }
    }
    println("Done!")

    // Emitting 1
    // 1
    // Emitting 2
    // 2
    // Done!
}


二、Flow 的常用操作符

1、末端流操作符 collect 、reduce 、fold、toxxx 等

fun terminalFlowOptFun() = runBlocking {
    val flow = (1..3).asFlow().onEach { delay(200) }
    flow.collect { println(it) }
    // 1
    // 2
    // 3

    val reduceSum = flow.reduce { a, b -> a + b }
    println("reduce: sum = $reduceSum")
    // reduce: sum = 6

    val foldSum = flow.fold(100) { a, b -> a + b }
    println("fold: sum = $foldSum")
    // fold: sum = 106

    val list = flow.toList()
    val set = flow.toSet()
    println("list: $list")
    println("set: $set")
    // list: [1, 2, 3]
    // set: [1, 2, 3]

    val flow2 = flowOf("one", "two").onEach { delay(200) }
    flow.onEach { println(it) }.launchIn(this)
    flow2.onEach { println(it) }.launchIn(this)
    // 1
    // one
    // 2
    // two
    // 3
}

2、流启动时 onStart

fun startFlowFun() = runBlocking {
    (1..3).asFlow()
        .onEach { delay(1000) }
        .onStart { println("onStart") }
        .collect { println(it) }

    // onStart
    // 1
    // 2
    // 3
}

3、流完成时 onCompletion

(1)使用 try ... finally 实现;
(2)通过 onCompletion 函数实现。

fun completionFlowFun() = runBlocking {
    try {
        flow {
            for (i in 1..3) {
                delay(1000)
                emit(i)
            }
        }.collect {
            println(it)
        }
    } finally {
        println("Done!")
    }
    // 1
    // 2
    // 3
    // Done!

    flow {
        for (i in 1..3) {
            delay(1000)
            emit(i)
        }
    }.onCompletion { println("Done!") }
        .collect { println(it) }
    // 1
    // 2
    // 3
    // Done!
}

4、背压 Backpressure

Backpressure 是响应式编程的功能之一,Flow 的 Backpressure 是通过 suspend 函数实现的。
(1)buffer 缓冲(这里要注意的是,buffer 的容量是从 0 开始计算的
   - SUSPEND 设置缓冲区,如果溢出了,则将当前协程挂起,直到有消费了缓冲区中的数据;
   - DROP_LATEST 设置缓冲区,如果溢出了,丢弃最新的数据;
   - DROP_OLDEST 设置缓冲区,如果溢出了,丢弃最老的数据。
(2)conflate 合并
   - 不设缓冲区,也就是缓冲区大小为 0,采取 DROP_OLDEST 策略,等价于 buffer(0, BufferOverflow.DROP_OLDEST) 。

fun bufferFlowFun() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow()
            .onEach {
                delay(100)
                println("produce data: $it")
            }
            .buffer(1, BufferOverflow.SUSPEND)
            .collect {
                delay(500)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")

    // produce data: 1
    // produce data: 2
    // produce data: 3
    // collect: 1
    // produce data: 4
    // collect: 2
    // produce data: 5
    // collect: 3
    // collect: 4
    // collect: 5
    // cosTime: 2742
}
fun conflateFlowFun() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow()
            .onEach {
                delay(100)
                println("produce data: $it")
            }
            .conflate()
            .collect {
                delay(500)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")

    // produce data: 1
    // produce data: 2
    // produce data: 3
    // produce data: 4
    // produce data: 5
    // collect: 1
    // collect: 5
    // cosTime: 1223
}

5、Flow 异常处理 catch、retry、retryWhen

(1)catch 操作符捕获上游异常
   - onCompletion 用来处理 Flow 是否收集完成,即使是遇到异常也会执行;
   - onCompletion 有一个参数可以用来判断上游是否出现异常;上游出现异常,不为 null,未出现异常,则为 null;
   - onCompletion 只能判断是否出现了异常,并不能捕获异常;
   - 捕获异常使用 catch 操作符;
   - 如果把 onCompletion 和 catch 交换一下位置,则 catch 操作捕获到异常之后,不会再影响下游;
   - catch 操作符用于实现异常透明化处理, catch 只是中间操作符不能捕获下游的异常;
   - catch 操作符内,可以使用 throw 再次抛出异常、可以使用 emit() 转换为发射值、可以用于打印或者其他业务逻辑的处理等等。
(2)retryretryWhen 操作符重试
   - 如果上游遇到了异常,并且 retry 方法返回 true 则会进行重试,最多重试 retries 指定的次数;
   - retry 最终调用的是 retryWhen 操作符。

fun catchFlowFun() = runBlocking {
    (1..5).asFlow()
        .onEach {
            if (it == 4) {
                throw Exception("test exception")
            }
            delay(100)
            println("produce data: $it")
        }
        /*.catch { e ->
            println("catch exception: $e")
        }*/
        .onCompletion { e ->
            if (null == e) {
                println("onCompletion")
            } else {
                println("onCompletion: $e")
            }
        }
        .catch { e ->
            println("catch exception: $e")
        }
        .collect {
            println("collect: $it")
        }

    // produce data: 1
    // collect: 1
    // produce data: 2
    // collect: 2
    // produce data: 3
    // collect: 3
    // onCompletion: java.lang.Exception: test exception
    // catch exception: java.lang.Exception: test exception
}
fun retryFlowFun() = runBlocking {
    (1..5).asFlow()
        .onEach {
            if (it == 2) {
                throw Exception("test exception")
            }
            delay(100)
            println("produce data: $it")
        }
        .retry(1) {
            it.message == "test exception"
        }
        /*.retryWhen { cause, attempt ->
            cause.message == "test exception" && attempt < 1
        }*/
        .catch { ex ->
            println("catch exception: ${ex.message}")
        }
        .collect {
            println("collect: $it")
        }

    // produce data: 1
    // collect: 1
    // produce data: 1
    // collect: 1
    // catch exception: test exception
}

6、Flow 线程切换 flowOn

(1)响应线程是由 CoroutineContext 决定的,比如,在 Main 线程中执行 collect, 那么响应线程就是 Dispatchers.Main;
(2)Flow 通过 flowOn 方法来切换线程,多次调用,都会影响到它上游的代码。

fun switchThreadFlowFun() = runBlocking {
    val myDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    (1..2).asFlow()
        .onEach {
            printlnWithThread("produce data: $it")
        }
        .flowOn(Dispatchers.IO)
        .onEach {
            printlnWithThread("IO data: $it")
        }
        .flowOn(myDispatcher)
        .onEach {
            printlnWithThread("my data: $it")
        }
        .flowOn(Dispatchers.Default)
        .onCompletion {
            myDispatcher.close()
        }
        .collect {
            printlnWithThread("collect: $it")
        }

    // Thread -> id: 12, name: DefaultDispatcher-worker-2, produce data: 1
    // Thread -> id: 12, name: DefaultDispatcher-worker-2, produce data: 2
    // Thread -> id: 13, name: pool-1-thread-1, IO data: 1
    // Thread -> id: 13, name: pool-1-thread-1, IO data: 2
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, my data: 1
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, my data: 2
    // Thread -> id: 1, name: main, collect: 1
    // Thread -> id: 1, name: main, collect: 2
}

7、Flow 的中间转换操作符

(1)map 操作符用于将流中的每个元素进行转换后再发射出来

fun mapFlowFun() = runBlocking {
    (1..2).asFlow()
        .map {
            "map -> $it"
        }
        .collect {
            println(it)
        }

    // map -> 1
    // map -> 2
}

(2)transform 操作符,可以任意多次调用 emit ,这是 transform 跟 map 最大的区别

fun transformFlowFun() = runBlocking {
    (1..2).asFlow()
        .transform {
            emit("transform1 -> $it")
            delay(100)
            emit("transform2 -> $it")
        }
        .collect {
            println(it)
        }

    // transform1 -> 1
    // transform2 -> 1
    // transform1 -> 2
    // transform2 -> 2
}

(3)onEach 遍历

fun onEachFlowFun() = runBlocking {
    (1..3).asFlow()
        .onEach { println("onEach: $it") }
        .collect { println(it) }

    // onEach: 1
    // 1
    // onEach: 2
    // 2
    // onEach: 3
    // 3
}

(4)filter 条件过滤

fun filterFlowFun() = runBlocking {
    (1..5).asFlow()
        .filter { it % 2 == 0 }
        .collect { println(it) }

    // 2
    // 4
}

(5)drop 过滤掉 前 N 个 元素

fun dropFlowFun() = runBlocking {
    (1..5).asFlow()
        .drop(3)
        .collect { println(it) }

    // 4
    // 5
}

(6)dropWhile 过滤 满足条件前 N 个 元素,一旦条件不满足则不再过滤后续元素

fun dropWhileFlowFun() = runBlocking {
    listOf(1, 3, 4, 2, 5).asFlow()
        .dropWhile { it < 4 }
        .collect { println(it) }
    // 4
    // 2
    // 5

    listOf(1, 3, 4, 2, 5).asFlow()
        .dropWhile { it % 2 == 1 }
        .collect { println(it) }
    // 4
    // 2
    // 5
}

(7)take 只取 前 N 个 emit 发射的值

fun takeFlowFun() = runBlocking {
    (1..5).asFlow()
        .take(2)
        .collect { println(it) }

    // 1
    // 2
}

(8)takeWhile 只取 满足条件前 N 个 元素,一旦条件不满足则不再获取后续元素

fun takeWhileFlowFun() = runBlocking {
    (5 downTo 1).asFlow()
        .takeWhile { it > 3 }
        .collect { println(it) }

    // 5
    // 4

    listOf(5, 2, 4, 1).asFlow()
        .takeWhile { it > 3 }
        .collect { println(it) }

    // 5
}

(9)zip 是可以将2个 flow 进行合并的操作符
   - 即使 flowB 中的每一个 item 都使用了 delay() 函数,在合并过程中也会等待 delay() 执行完后再进行合并;
   - 如果 flowA 和 flowB 中 item 个数不一致,则合并后新的 flow item 个数,等于较小的 item 个数。

fun zipFlowFun() = runBlocking {
    val flowA = (1..6).asFlow()
    val flowB = flowOf("one", "two", "three").onEach { delay(200) }
    flowA.zip(flowB) { a, b -> "$a and $b" }
        .collect { println(it) }

    // 1 and one
    // 2 and two
    // 3 and three
}

(10)combine 合并时,组合每个流最新发出的元素

fun combineFlowFun() = 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
}

(11)flattenConcat 将给定流按顺序展平为单个流,而不交错嵌套流

fun flattenConcatFlowFun() = runBlocking {
    val flowA = (1..3).asFlow()
    val flowB = flowOf("a", "b", "c").onEach { delay(1000) }
    flowOf(flowA, flowB).flattenConcat().collect { println(it) }

    // 1
    // 2
    // 3
    // a
    // b
    // c
}

(12)fattenMerge 有一个参数,并发限制,默认 16;参数必须大于0,为 1 时,等价于 flattenConcat

fun flattenMergeFlowFun() = runBlocking {
    val flowA = (1..3).asFlow().onEach { delay(1000) }
    val flowB = flowOf("a", "b", "c").onEach { delay(2000) }
    flowOf(flowA, flowB).flattenMerge(8).collect { println(it) }

    // 1
    // a
    // 2
    // 3
    // b
    // c
}

(13)flatMapContact 由 map、flattenConcat 操作符实现,收集新值之前会等待 flatMapConcat 内部的 flow 完成

fun flatMapContactFlowFun() = runBlocking {
    (1..2).asFlow()
        .flatMapConcat {
            flow {
                emit(it)
                delay(1000)
                emit("string: $it")
            }
        }
        .collect {
            println(it)
        }

    // 1
    // string: 1
    // 2
    // string: 2
}

(14)flatMapMerge 由 map、flattenMerge 操作符实现,不会等待内部的 flow 完成

fun flatMapMergeFlowFun() = runBlocking {
    (1..2).asFlow()
        .flatMapMerge {
            flow {
                emit(it)
                delay(1000)
                emit("string: $it")
            }
        }
        .collect {
            println(it)
        }

    // 1
    // 2
    // string: 1
    // string: 2
}

(15)flatMapLatest 当发射了新值之后,上个 flow 就会被取消

fun flatMapLatestFlowFun() = runBlocking {
    (1..3).asFlow()
        .onEach { delay(100) }
        .flatMapLatest {
            flow {
                println("begin flatMapLatest: $it")
                delay(200)
                emit("string: $it")
                println("end flatMapLatest: $it")
            }
        }
        .collect { println(it) }
}


三、StateFlow 和 SharedFlow

StateFlowSharedFlow 是用来替代 BroadcastChannel 的新的 API。用于上游发射数据,能同时被 多个订阅者 收集数据。

1、StateFlow

(1)StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新;还可通过其 value 属性读取当前状态值;
(2)StateFlow 有两种类型: StateFlow(只读) 和 MutableStateFlow(可以改变 value 的值);
(3)StateFlow 的状态由其值表示,任何对值的更新都会反馈新值到所有流的接收器中;
(4)StateFlow 发射的数据可以被在不同的协程中的多个接受者同时收集;
(5)StateFlow 是热流,只要数据发生变化,就会发射数据;
(6)StateFlow 调用 collect 收集数据后不会停止,需要手动取消订阅者的协程;
(7)StateFlow 只会发射最新的数据给订阅者。

class StateFlowTest {
    private val _state = MutableStateFlow("unKnow")
    val state: StateFlow<String> get() = _state

    fun getApi1(scope: CoroutineScope) {
        scope.launch {
            delay(1000)
            _state.value = "hello StateFlow"
        }
    }

    fun getApi2(scope: CoroutineScope) {
        scope.launch {
            delay(1000)
            _state.value = "hello Kotlin"
        }
    }
}

fun stateFlowFun() = runBlocking {
    val test = StateFlowTest()
    test.getApi1(this)
    delay(1000)
    test.getApi2(this)

    val job1 = launch(Dispatchers.IO) {
        delay(5000)
        test.state.collect {
            printlnWithThread(it)
        }
    }
    val job2 = launch(Dispatchers.IO) {
        delay(5000)
        test.state.collect {
            printlnWithThread(it)
        }
    }

    delay(7000)
    job1.cancel()
    job2.cancel()

    // Thread -> id: 11, name: DefaultDispatcher-worker-1, hello Kotlin
    // Thread -> id: 13, name: DefaultDispatcher-worker-3, hello Kotlin
}

2、SharedFlow

(1)SharedFlow 管理一系列状态更新(即事件流),而非管理当前状态;
(2)SharedFlow 也有两种类型:SharedFlowMutableSharedFlow
   - SharedFlow 包含可用作原子快照的 replayCache,每个新的订阅者会先从 replay cache 中获取值,然后才收到新发出的值;
   - MutableSharedFlow 可用于从挂起或非挂起的上下文中发射值,顾名思义,可以重置 replayCache,而且还将订阅者的数量作为 Flow 暴露出来。
(3)MutableSharedFlow 具有 subscriptionCount 属性,其中包含处于活跃状态的收集器的数量;
(4)MutableSharedFlow 包含一个 resetReplayCache 函数,在不想重放已向数据流发送的最新信息的情况下使用;
(5)使用 sharedIn 方法可以将 Flow 转换为 SharedFlow

class SharedFlowTest {
    private val _state = MutableSharedFlow<Int>(
        replay = 2,                               // 当新的订阅者 Collect 时,发送几个已经发送过的数据给它
        extraBufferCapacity = 3,                  // 减去 replay 还缓存多少数据(即此处总缓存为5)
        onBufferOverflow = BufferOverflow.SUSPEND // 缓存溢出时的处理策略,三种 丢掉最新值、丢掉最旧值和挂起
    )
    val state: SharedFlow<Int> get() = _state

    fun getApi(scope: CoroutineScope) {
        scope.launch {
            for (i in 0..5) {
                delay(200)
                _state.emit(i)
                printlnWithThread("send data: $i")
            }
        }
    }
}

fun sharedFlowFun() = runBlocking {
    val test = SharedFlowTest()
    test.getApi(this)

    val job = launch(Dispatchers.IO) {
        delay(1000)
        test.state.collect {
            printlnWithThread("collect data: $it")
        }
    }

    delay(5000)
    job.cancel()

    // 总缓存为5,订阅时先发送2个旧数据,然后再收集新数据
    // Thread -> id: 1, name: main, send data: 0
    // Thread -> id: 1, name: main, send data: 1
    // Thread -> id: 1, name: main, send data: 2
    // Thread -> id: 1, name: main, send data: 3
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 2
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 3
    // Thread -> id: 1, name: main, send data: 4
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 4
    // Thread -> id: 1, name: main, send data: 5
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 5
}
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容