kotlin-协程(七)关于Flow

1,548 阅读5分钟

一、Flow的创建方式

常见的创建Flow的方式有flow{}flowof{}asFlow等,下面是示例代码:

fun main() {
    runBlocking {
        //方式一
        flow<Int> {
            emit(0)
        }.collect {
            printMsg(it.toString())
        }

        //方式二
        flowOf(1,2,3).collect{
            printMsg(it.toString())
        }

        //方式三
        listOf(4,5,6).asFlow().collect{
            printMsg(it.toString())
        }
    }
}

//日志
main @coroutine#1 0
main @coroutine#1 1
main @coroutine#1 2
main @coroutine#1 3
main @coroutine#1 4
main @coroutine#1 5
main @coroutine#1 6

二、Flow的常见操作符

1、filter,map,take

比较常用也比较简单,代码后面的例子会体现,此处略。

2、onStart,onCompletion

onStartonCompletion是协程创建和完成时的回调,与在事件处理的上下游位置无关

flowOf(1, 2, 3, 4)
    .filter { it > 1 }   //过滤
    .onStart { printMsg("Flow onStart") }
    .onCompletion { printMsg("Flow onCompletion $it") }
    .map { it * 2 }    //变换
    .take(2)   //截取
    .collect {
        printMsg("Flow result:$it")
    }
//日志
main @coroutine#1 Flow onStart
main @coroutine#1 Flow result:4
main @coroutine#1 Flow result:6
main @coroutine#1 Flow onCompletion kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted, no more elements needed

尝试在中途抛出一个异常

flowOf(1, 2, 3, 4)
    .filter { it > 1 }   //过滤
    .onStart { printMsg("Flow onStart") }
    .onCompletion { printMsg("Flow onCompletion $it") }
    .map { it * 2 }    //变换
    .take(2)   //截取
    .collect {
        printMsg("Flow result:$it")
        //收到第一个数据就抛出异常
        throw IllegalStateException()        <----------------------变化在这里
    }
//日志,程序报错
main @coroutine#1 Flow onStart
main @coroutine#1 Flow result:4
main @coroutine#1 Flow onCompletion java.lang.IllegalStateException      <------打印异常信息
Exception in thread "main" java.lang.IllegalStateException
...略...

3、catch异常处理

如果flow出现异常应该如何捕获异常? 看二段代码:

第一段

val flow = flow {
    emit(1)
    throw IllegalStateException()    <-------------------发送数据抛异常
    emit(2)
}
flow.catch {      <----------------------这里catch
    printMsg("Flow catch $it")
}.onCompletion {
    printMsg("Flow onCompletion $it")
}.collect{        <-------------结束操作符
    printMsg("Flow collect $it")
}

//日志
main @coroutine#1 Flow collect 1
main @coroutine#1 Flow catch java.lang.IllegalStateException
main @coroutine#1 Flow onCompletion null

第二段

flowOf(1, 2)
    .catch { printMsg("Flow catch $it") }        <------------------这里catch
    .onCompletion { printMsg("Flow onCompletion $it") }
    .collect {                    <-------------结束操作符
        printMsg("Flow collect $it")
        //收到第一个数据就抛出异常
        throw IllegalStateException()       <---------------处理数据抛异常
    }
    
//日志,程序报错
main @coroutine#1 Flow collect 1
main @coroutine#1 Flow onCompletion java.lang.IllegalStateException
Exception in thread "main" java.lang.IllegalStateException
...略...

所以要注意发送时候的异常是可以捕获的,但是处理数据时的异常是没有办法捕获的。catch 的作用域,仅限于 catch 的上游。

4、flowOn、launchIn

flowOnlaunchIn主要作用是切换线程。但是它们的使用有一些需要注意的地方。

(1)、flowOn

flow {
    emit(1)
    emit(2)
}.filter {
    printMsg("filter $it")
    it > 1
}.flowOn(Dispatchers.IO)     <----------切换线程
.map {
    printMsg("map $it")
    it * 2
}.collect {
    printMsg("collect $it")
}

//日志
DefaultDispatcher-worker-1 @coroutine#2 filter 1    <------filter在子线程
DefaultDispatcher-worker-1 @coroutine#2 filter 2    <------filter在子线程
main @coroutine#1 map 2                             <------map在主线程
main @coroutine#1 collect 4                         <------collect在主线程

可以看到flowOn切换线程只在它的上游起作用。

(2)、launchIn

如果想让flowOn上游的代码在子线程执行,而flowOn下面的代码在主线程执行怎么办?下面是一个例子:

lifecycleScope.launch(Dispatchers.Main) {   <------lifecycleScope上下文所在的线程是主线程main
    flow {                                 _______
        log("flow emit 1")                    |
        emit(1)                               |
        log("flow emit 2")                    |
        emit(2)                           IO线程执行
    }.map {                                   |
        log("flow map $it")                   |
        it * 2                                |
    }.flowOn(Dispatchers.IO)               ___|____
        .onEach {                             |
            log("flow onEach $it")            |
        }.onCompletion {                      |
            log("flow onCompletion $it")  Main线程执行
        }.catch {                             |
            log("flow catch $it")             |
        }.launchIn(lifecycleScope)            |
}                                         ____|___

//日志
flow emit 1 线程:DefaultDispatcher-worker-1 @coroutine#4
flow map 1 线程:DefaultDispatcher-worker-1 @coroutine#4
flow emit 2 线程:DefaultDispatcher-worker-1 @coroutine#4
flow map 2 线程:DefaultDispatcher-worker-1 @coroutine#4
flow onEach 2 线程:main @coroutine#3
flow onEach 4 线程:main @coroutine#3
flow onCompletion null 线程:main @coroutine#3

launchIn将它上游的代码切换到给定上下文lifecycleScope的线程执行,这样就可以让上面的业务代码在IO线程执行,而结果回调到UI线程执行。

launchIn的源码上是这样描述的:Note that resulting value of launchIn is not used the provided scope takes care of cancellation(机翻:注意,结果值launchIn不会被使用,提供的作用域负责取消).,重点在后一句话,也就是Flow将随着给定的作用域的取消而取消。看下面一个EditText输入框的扩展封装:

fun EditText.textChangeFlow(): Flow<String> = callbackFlow {  //将回调转换为Flow
    val watcher = object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {}
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            s?.let { offer(it.toString()) }   //分发数据
        }
    }
    addTextChangedListener(watcher)   //添加监听器
    awaitClose { removeTextChangedListener(watcher) } //挂起避免协程退出,保证流一直运行,当退出时会回调block
}

//使用:比如在Activity中使用
editText.textChangeFlow()
              .debounce(500)   //500毫秒内只发送一次最新的值
              .flowOn(Dispatchers.IO)  //如果还有其他复杂操作需要放在子线程
              .onEach {
                   //TODO do something
                }
                .launchIn(lifecycleScope)   //当页面退出时Flow也会退出

思路是通过callbackFlow将结果转化为Flow流,然后进一步做防抖、过滤、变换等操作,最后通过flowOnlaunchIn实现线程切换和协程生命周期的管理。

5、retry,takeWhile,buffer

retry异常时重试次数,示例如下:

fun main() = runBlocking {
    flow {
        printMsg("emit 1")
        emit(1)
        printMsg("emit 2")
        emit(2)
    }.onEach {
        printMsg("onEach $it")
        if (it == 2) {
            throw RuntimeException("Exception on $it") // 抛出异常
        }
    }.retry(1) // 重试2次
        .catch { printMsg("catch $it") }
        .collect { printMsg("collect $it") }
}

//日志
main @coroutine#1 emit 1
main @coroutine#1 onEach 1
main @coroutine#1 collect 1
main @coroutine#1 emit 2
main @coroutine#1 onEach 2
main @coroutine#1 emit 1
main @coroutine#1 onEach 1
main @coroutine#1 collect 1
main @coroutine#1 emit 2
main @coroutine#1 onEach 2
main @coroutine#1 catch java.lang.RuntimeException: Exception on 2

注意retry重试的是整个代码块,代码中的1其实是没有问题的也重试了。

takeWhile - 按照给定的条件从流中获取元素,直到条件不成立

fun main() = runBlocking<Unit> {
    // 输出小于5的数字
    (1..10)
        .asFlow()
        .takeWhile { it < 5 }
        .collect { println(it) } // 输出 1 2 3 4
}

buffer - 在中间处理和收集操作之间缓冲流

.buffer() // 缓冲流

6、zip,combine

zipcombine组合不同flow的元素

fun main() = runBlocking {
    val nums = (1..4).asFlow()
    val strs = flowOf("one", "two", "three")
    nums.zip(strs) { a, b -> "$a -> $b" }
        .collect { printMsg(it) }
}

//日志
main @coroutine#1 1 -> one
main @coroutine#1 2 -> two
main @coroutine#1 3 -> three

替换上面的zipcombine,输出日志为:

main @coroutine#1 1 -> one
main @coroutine#1 2 -> two
main @coroutine#1 3 -> three
main @coroutine#1 4 -> three       <------区别主要体现在这里

zipcombine操作符的区别在于它们组合Flow的方式。zip操作符会按顺序一一对应地组合两个Flow中的项,而combine操作符则会将每个Flow中的最新项作为参数应用于给定的lambda函数。因此,zip操作符只有当两个Flow的项数相同时才会产生输出,而combine操作符则可以组合项数不同的Flow并能立即输出组合结果。

7、flatMapConcat,flatMapMerge

这二个操作符都属于展平流,如何理解和区别呢? 看下二个操作符的代码

fun main() = runBlocking {
    (1..2).asFlow()
        .flatMapConcat {
            //将元素转化成了一个新的flow
            zipElement(it)
        }.collect { printMsg(it) }
}

//将元素转化成了一个新的flow
fun zipElement(int: Int): Flow<String> =
    flow {
        emit("$int -> Hello")
        delay(1000)
        emit("$int -> Kotlin")
    }
    
//日志
main @coroutine#1 1 -> Hello
main @coroutine#1 1 -> Kotlin
main @coroutine#1 2 -> Hello
main @coroutine#1 2 -> Kotlin

替换上面的flatMapConcatflatMapMerge,输出日志为:

main @coroutine#1 1 -> Hello
main @coroutine#1 2 -> Hello
main @coroutine#1 1 -> Kotlin
main @coroutine#1 2 -> Kotlin

flatMapConcatflatMapMerge操作符都是用于将一个Flow的项转换为其他Flow的操作符,但使用的组合策略不同。

flatMapConcat操作符会顺序地组合每个转换后的Flow,即它会等待前一个转换后的Flow完成后再开始下一个转换。因此,它会按顺序发出所有转换后的Flow的项。常用于串行的数据传递和转换,比如串行的网络请求等。

flatMapMerge操作符会并发地组合每个转换后的Flow,即它会立即开始下一个转换而不等待前一个转换完成。因此,它会立即发出所有转换后的Flow的项,并通过任意顺序发射它们。

8、sample,debounce

sample 操作符会在一定的时间间隔内,发射最新的元素。例如,如果你设置了一个 500ms 的间隔,那么该操作符会每隔 500ms 发射最近的元素。因此, sample 操作符通常用于控制发射元素的数量和频率。

另一方面, debounce 操作符需要等待在一定时间内没有新的元素,才会将最后一个元素发射出去。例如,如果你设置了 500ms 的时间间隔,那么如果 500ms 内有新元素到达,那么之前的元素都会被忽略,只有最新的元素会发射出去。因此, debounce 操作符通常用于禁止在时间间隔内连续发射相同的元素。

综上所述, sample 操作符会在一定的时间间隔内,发射最新的元素,而debounce 操作符则需要等待在一定时间内没有新的元素,才会将最后一个元素发射出去。这两个操作符有着不同的用途,需要根据具体情况来选择使用。

fun main() = runBlocking {
    //sample
    flowOf("A", "B", "C", "D", "E", "F")
        .onEach { delay(100) }
        .sample(250)
        .collect { printMsg(it) }

    // 使用 debounce 操作符来从一个 Flow 中去重连续发送的相同消息
    flowOf(1, 2, 2, 3, 3, 3, 4, 4, 4, 4)
        .onEach { delay(100) }
        .debounce(150) // 用于禁止在 150ms 内发射相同值的操作符
        .collect { printMsg("Distinct value: $it") }
}

fun printMsg(msg: String) {
    println("${Thread.currentThread().name} $msg")
}

//日志
main @coroutine#1 B
main @coroutine#1 D
main @coroutine#1 Distinct value: 4

9、distinctUntilChanged

过滤重复的值

val flow = flowOf(1, 2, 2, 3, 3, 3, 4, 5, 5)

// 使用 distinctUntilChanged 过滤连续重复的元素
flow.distinctUntilChanged().collect {
    printMsg("flow $it")
}

//日志
main @coroutine#1 flow 1
main @coroutine#1 flow 2
main @coroutine#1 flow 3
main @coroutine#1 flow 4
main @coroutine#1 flow 5

三、用Flow做串行与并行

1、串行

方法有很多种,这里讲二种。

比如下面多个方法有多种返回值类型,借助密封类和Flow做串行处理,代码如下:

suspend fun method1(): Int {   //返回Int
    delay(1000)   //模拟方法耗时
    return 2
}

suspend fun method2(): String {   //返回String
    delay(2000)   //模拟方法耗时
    return "Hello"
}

suspend fun method3(): Person {   //返回data class对象
    delay(2000)   //模拟方法耗时
    return Person("Android", 18)
}

//密封类包装三种类型
sealed class DataResult {
    data class IntValue(val value: Int) : DataResult()
    data class StringValue(val value: String) : DataResult()
    data class PersonData(val person: Person) : DataResult()
}

//具体串行处理的代码
lifecycleScope.launch {
    flow {
        emit(DataResult.IntValue(method1()))
        emit(DataResult.StringValue(method2()))
        emit(DataResult.PersonData(method3()))
    }.flowOn(Dispatchers.IO)
        .collect {
            when (it) {
                is DataResult.IntValue -> {
                    "DataResult.IntValue:$it".logd
                }

                is DataResult.StringValue -> {
                    "DataResult.StringValue:$it".logd
                }

                is DataResult.PersonData -> {
                    "DataResult.PersonData:${it.toString()}".logd
                }
            }
        }
}

//日志
11:48:53.163 10970-10970  DataResult.IntValue:IntValue(value=2)
11:48:55.163 10970-10970  DataResult.StringValue:StringValue(value=Hello)
11:48:57.168 10970-10970  DataResult.PersonData:PersonData(person=Person(userName='Android', age=18))

这样写主要是提供一种密封类的思路,但在实际开发中既然是串行,那一般是要根据前一次方法执行的结果来判断下一次需要执行的方法,接下来换另一种方式:

lifecycleScope.launch {
    flowOf(method1())
        .flatMapLatest {
            "flowOf,method1:$it".logd
            flowOf(method2())
        }
        .flatMapLatest {
            "flowOf,method2:$it".logd
            flowOf(method3())
        }
        .flowOn(Dispatchers.IO)
        .collect {
            "flowOf,method3:${it.toString()}".logd
        }
}

//日志
13:54:27.501 13264-13308      flowOf,method1:2
13:54:29.509 13264-13306      flowOf,method2:Hello
13:54:31.527 13264-13264      flowOf,method3:Person(userName='Android', age=18)

这种方式使用了flow的操作符flatMapLatest,当有新的元素到来时,取消上一个Flow。也不需要额外使用包装类。

方法还有很多,欢迎留言。

2、并行

并行的话首先肯定推荐用协程async,看下面的例子:

//3个方法
suspend fun method1(): Int {
    delay(1000)
    return 2
}

suspend fun method2(): String {
    delay(2000)
    return "Hello"
}

suspend fun method3(): Person {
    delay(3000)
    return Person("Android", 18)
}

//并行执行
lifecycleScope.launch {
    "async value:start".logd
    val asyncList = listOf(
        async { method1() },
        async { method2() },
        async { method3() }
    )

    asyncList.awaitAll().onEach {
        "async value:${it.toString()}".logd
    }
    "async value:end".logd
}

//日志
14:13:01.651 14566-14566 ACLog      async value:start
14:13:04.656 14566-14566 ACLog      async value:2
14:13:04.656 14566-14566 ACLog      async value:Hello
14:13:04.657 14566-14566 ACLog      async value:Person(userName='Android', age=18)
14:13:04.657 14566-14566 ACLog      async value:end

总耗时3秒左右,数据按方法顺序输出,符合我们的预期。那用Flow怎么做? 下面是一个例子:

lifecycleScope.launch {
    "flowOf:start".logd
    val flow1 = flow { emit(method1()) }
    val flow2 = flow { emit(method2()) }
    val flow3 = flow { emit(method3()) }

    //使用zip组合Flow,让Flow并发执行并整合结果
    val combinedFlow = flow1.zip(flow2) { result1, result2 ->
        result1 to result2
    }.zip(flow3) { (result1, result2), result3 ->
        Triple(result1, result2, result3)
    }

  combinedFlow.collect{
      "flowOf:result:${it.first},${it.second},${it.third}".logd
  }
    "flowOf:end".logd
}

//日志
15:16:29.687 19555-19555  flowOf:start
15:16:32.691 19555-19555  flowOf:result:2,Hello,Person(userName='Android', age=18)
15:16:32.693 19555-19555  flowOf:end

使用 zip 操作符将这三个 Flow 组合在一起,最后通过Triple返回结果,总耗时3秒左右,且按方法的执行顺序拿到了结果,符合我们的预期。

四、StateFlow

1、StateFlow,MutableStateFlow

MutableStateFlowStateFlow 的子类,与其具有相同的行为和功能,同时还具有一些不同之处。与 StateFlow 只能在创建时设置其初始值不同,MutableStateFlow 可以在任何时候更改其值。这使得 MutableStateFlow 更适合于表示可变状态。于是我们在使用时经常可以看到如下的代码:

class MyViewModel : ViewModel() {
    //可变的设置为私有的不对外暴露
    private val _counter = MutableStateFlow(0)
    //不可变的对外暴露
    val counter: StateFlow<Int> = _counter或_counter.asStateFlow()

    fun incrementCounter() {
        //修改值
        _counter.value++
    }
}

class MainActivity : AppCompatActivity() {

    private val viewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.counter.collect { count ->
            // Display the current count
        }

        button.setOnClickListener {
            viewModel.incrementCounter()
        }
    }
}

2、简易案例

下面继续看一个搜索防抖的雏形案例,简易代码如下:

class MainActivity : AppCompatActivity() {

    private val etFlow = MutableStateFlow("et_Hint")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //打印协程名称
        System.setProperty("kotlinx.coroutines.debug", "on")
        val et = findViewById<AppCompatEditText>(R.id.et)
        et.doAfterTextChanged {
            //如果不为空或者null就发射流
            if (!TextUtils.isEmpty(it)) {
                etFlow.value = it.toString()
            }
        }

        lifecycleScope.launch {
            //在1秒采样周期内只发射最新的值
            etFlow.sample(1000).collect {
                //获取值去搜索,这里就打印日志
                log(it)
            }
        }
    }

    /**
     * 打印日志
     */
    private fun log(msg: String) {
        Log.d("LOG_PRINT", "内容:$msg 线程:${Thread.currentThread().name}")
    }

}

//日志
内容:et_Hint 线程:main @coroutine#2
内容:123 线程:main @coroutine#2

可以看到创建MutableStateFlow需要给默认参数,上面代码中是et_Hint,并且这个默认值会首先发射出来,通过日志可以看到后面在EditText中输入123也打印出来了。

上面的代码其实有一个问题,当我们先输入1这个时候发射1,然后我们删除1输入框内容为空不发射任何数据,当我们再次输入1,这次的1是不会被发送的,这是因为给MutableStateFlow赋值,如果二次的数据的哈希值一样,后面的数据不会被发送(可以看下源码),这显然不符合搜索的需求,解决办法就是包一下并重写hashCode方法返回随机数让它每次的值都不相同:

data class MutableStateFlowBean(var content:String) {

    override fun equals(other: Any?): Boolean=false

    override fun hashCode(): Int {
        return Random.nextInt()
    }
}

3、使用场景

(1)在ViewModel中使用StateFlow

这是最常用的方式,可以参考上面三、1中的一段代码

(2)如何将StateFlow的值安全暴露给UI层,不受生命周期困扰?

可以通过asLiveData()StateFlow转化为LiveData
先导包implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

看如下的代码,定义一个按钮,发送5个数据,通过StateFlowasLiveData()和普通的LiveData接收:

class MainViewModel : ViewModel() {
    //flow
    private val mutableStateFlow = MutableStateFlow(0)
    val flowLiveData = mutableStateFlow.asLiveData()      <------这里asLiveData()

    //liveData
    val liveData = MutableLiveData<Int>()

    fun sendData(value: Int) {
        //flow发送值
        mutableStateFlow.value = value
        //livedata发送值
        liveData.value = value
    }

}
class MainActivity : AppCompatActivity() {

    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //打印协程名称
        System.setProperty("kotlinx.coroutines.debug", "on")

        //点击按钮发送5个数据
        val bt = findViewById<AppCompatButton>(R.id.bt)
        bt.setOnClickListener {
            lifecycleScope.launch {
                (1..5).forEach {
                    delay(1500)
                    log("sendData:$it")
                    mainViewModel.sendData(it)
                }
            }
        }

        //flow转的livedata
        mainViewModel.flowLiveData.observe(this) {
            log("flowLiveData:$it")
        }

        //livedata
        mainViewModel.liveData.observe(this) {
            log("liveData:$it")
        }
    }
}

中途息屏一段时间再亮屏,日志输出如下:

内容:flowLiveData:0 线程:main @coroutine#1   <------asLiveData后flowLiveData收到了默认值0
内容:sendData:1 线程:main @coroutine#4       <------点击按钮开始发射数据
内容:flowLiveData:1 线程:main @coroutine#1   
内容:liveData:1 线程:main @coroutine#4
内容:sendData:2 线程:main @coroutine#4
内容:flowLiveData:2 线程:main @coroutine#1
内容:liveData:2 线程:main @coroutine#4
内容:sendData:3 线程:main @coroutine#4       <------息屏,flowLiveData和liveData都没接收数据
内容:sendData:4 线程:main @coroutine#4
内容:sendData:5 线程:main @coroutine#4
内容:flowLiveData:5 线程:main @coroutine#6   <------亮屏,flowLiveData和liveData都接收到数据
内容:liveData:5 线程:main

除了先会收到默认值,其他的符合预期。

(3) 一处接收多处订阅

比如通过MQTT接收机器的多种状态,然后很多页面都需要订阅机器的实时状态,那么可以尝试这样做。

定义一个单例的类,类里面有设置Flow值的方法,也有二个获取Flow值的方法,其中一个关心页面的生命周期,另一个不关心:

class SimpleFlowClient {

    private val mutableStateFlow = MutableStateFlow("offline")
    private val stateFlow: StateFlow<String> = mutableStateFlow

    /**
     * 单例模式
     */
    companion object {
        private var instance: SimpleFlowClient? = null

        @Synchronized
        fun getInstance(): SimpleFlowClient {
            return instance ?: SimpleFlowClient().also { instance = it }
        }
    }

    /**
     * 设置Flow的值
     */
    fun setFlowValue(value: String) {
        mutableStateFlow.value = value
    }

    /**
     * 获取Flow的值,不关心页面的生命周期,
     * 使用LifecycleCoroutineScope是页面关闭时同时关闭协程
     * @param scope lifecycleScope
     */
    fun getFlowValue(
        scope: LifecycleCoroutineScope,
        result: (value: String) -> Unit = { _ -> }
    ) {
        scope.launch {
            stateFlow.collect {
                result.invoke(it)
            }
        }
    }

    /**
     * 获取Flow的值,转换成LiveData,关心页面的生命周期
     * @param owner lifecycleScope
     */
    fun getFlowValueAsLiveData(
        owner: LifecycleOwner,
        observer: (value: String) -> Unit = { _ -> }
    ) {
        stateFlow.asLiveData().observe(owner) {
            observer(it)
        }
    }
}

分别在MainActivityTwoActivity中做测试:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //打印协程名称
        System.setProperty("kotlinx.coroutines.debug", "on")

        //点击按钮跳转
        val bt = findViewById<AppCompatButton>(R.id.bt)
        bt.setOnClickListener {
            //跳转到TwoActivity
            Intent(this, TwoActivity::class.java).also { startActivity(it) }
            //2秒后修改了StateFlow的值
            Handler(Looper.myLooper()!!).postDelayed({
                SimpleFlowClient.getInstance().setFlowValue("online")
            }, 2000)
        }

        //跟踪Flow的数据
        SimpleFlowClient.getInstance().getFlowValue(lifecycleScope) {
            log("MainActivity:flow:$it")
        }

        //跟踪Flow的数据
        SimpleFlowClient.getInstance().getFlowValueAsLiveData(this) {
            log("MainActivity:asLiveData:$it")
        }
    }
}
class TwoActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_two)

        //跟踪Flow的数据
        SimpleFlowClient.getInstance().getFlowValue(lifecycleScope) {
            log("TwoActivity:flow:$it")
        }

        //跟踪Flow的数据
        SimpleFlowClient.getInstance().getFlowValueAsLiveData(this) {
            log("TwoActivity:asLiveData:$it")
        }

    }
}

日志如下:

------------------------进入MainActivity收到的值----------------------
内容:MainActivity:flow:offline 线程:main @coroutine#2
内容:MainActivity:asLiveData:offline 线程:main @coroutine#3


------------------------点击按钮进入TwoActivity收到的值------------------------
内容:TwoActivity:flow:offline 线程:main @coroutine#5
内容:TwoActivity:asLiveData:offline 线程:main @coroutine#6


------------------------在TwoActivity页面2秒后收到的值-------------------
内容:MainActivity:flow:online 线程:main @coroutine#2
内容:TwoActivity:flow:online 线程:main @coroutine#5
内容:TwoActivity:asLiveData:online 线程:main @coroutine#6


------------------------返回MainActivity页面后收到的值-------------------
内容:MainActivity:asLiveData:online 线程:main @coroutine#8

整体符合预期。

4、需要注意的问题

(1) StateFlow是SharedFlow高度封装的

跟下StateFlow的源码,可以看到有如下的代码:

/**
 * [StateFlow] that represents the number of subscriptions.
 *                                                                   ------看这里↓-------
 * It is exposed as a regular [StateFlow] in our public API, but it is implemented as [SharedFlow] undercover to
 * avoid conflations of consecutive updates because the subscription count is very sensitive to it.
 *
 * The importance of non-conflating can be demonstrated with the following example:
 * ```
 * val shared = flowOf(239).stateIn(this, SharingStarted.Lazily, 42) // stateIn for the sake of the initial value
 * println(shared.first())
 * yield()
 * println(shared.first())
 * ```
 * If the flow is shared within the same dispatcher (e.g. Main) or with a slow/throttled one,
 * the `SharingStarted.Lazily` will never be able to start the source: `first` sees the initial value and immediately
 * unsubscribes, leaving the asynchronous `SharingStarted` with conflated zero.
 *
 * To avoid that (especially in a more complex scenarios), we do not conflate subscription updates.
 */
private class SubscriptionCountStateFlow(initialValue: Int) : StateFlow<Int>,
    SharedFlowImpl<Int>(1, Int.MAX_VALUE, BufferOverflow.DROP_OLDEST)        <------注意这里
{
    init { tryEmit(initialValue) }

    override val value: Int
        get() = synchronized(this) { lastReplayedLocked }

    fun increment(delta: Int) = synchronized(this) {
        tryEmit(lastReplayedLocked + delta)
    }
}


public enum class BufferOverflow {
    /**
     * Suspend on buffer overflow.
     */
    SUSPEND,

    /**
     * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend.
     */
    DROP_OLDEST,                                                            ------看这里↑-------

    /**
     * Drop **the latest** value that is being added to the buffer right now on buffer overflow
     * (so that buffer contents stay the same), do not suspend.
     */
    DROP_LATEST
}

需要注意一些小细节:
① StateFlow是基于SharedFlow实现的,它是高度封装的SharedFlow。
② StateFlow的缓存容量为Int.MAX_VALUE
③ StateFlow的缓存策略为丢弃最旧的值且不挂起

(2) StateFlow中如何定义什么是新的值?

基于以上的源码,看下面的代码会输出什么?

fun main() {
    runBlocking {
        val mutableStateFlow = MutableStateFlow(0)

        //提前订阅的协程
        launch {
            mutableStateFlow.collect {
                printMsg("collect before $it")
            }
        }

        //修改StateFlow的值,分别修改为0-100
        launch {
            for (i in 0..100) {
                mutableStateFlow.value = i
                printMsg(" send  $i")
            }
        }

        //稍后订阅的协程
        launch {
            mutableStateFlow.collect {
                printMsg("collect after $it")
            }
        }
    }
}

//日志
main @coroutine#2 collect before 0
main @coroutine#3  send  0
main @coroutine#3  send  1
...省略send 2-98的输出...
main @coroutine#3  send  99
main @coroutine#3  send  100
main @coroutine#4 collect after 100
main @coroutine#2 collect before 100

可以看到提前订阅的协程的只收到了默认值0和最后的100,而稍后订阅的协程只收到了最后的100,中间的值都没有收到,这是为什么?

通过源码打断点,和官方的注释可以看到

image.png

image.png

只出现了0100二个值,其他的值都没有出现在断点中......官方注释(上图红框内)的解释是:
Here the coroutine could have waited for a while to be dispatched, so we use the most recent state here to ensure the best possible conflation of stale values 这里协程可能会等待一段时间才能被调度,所以我们在这里使用最近的状态来确保陈旧值的最佳可能合并

深层次的原因不得而知。如果改造成发送一个数据就挂起一次会怎么样呢? 把上面的代码做一个小小的修改:

//修改StateFlow的值,分别修改为0-100
launch {
    for (i in 0..100) {
        mutableStateFlow.value = i
        printMsg(" send  $i")
        delay(1)      <----------------改变在这里:加了一个挂起函数
    }
}

结果发现所有的中间值都收到了。根据现象可以得出结论:协程挂起或者执行完才会被判定有新的更新,就会更新StateFlow的值。

如果使用StateFlow发送了多个值,且中间的每个值都需要处理,那就需要注意上面的问题。如果多个值建议换成SharedFlow

(3) 为什么推荐直接给value赋值,而不是通过emit和tryEmit发射新的值?

首先,需要明确的是,StateFlow的状态值是不可变的。也就是说,一旦状态值被赋值,就不能再次修改。因此,如果我们使用emittryEmit方法来发射新的状态值,就需要创建一个新的对象来代表新的状态值。这样做会导致内存分配和垃圾回收的开销,从而影响程序的性能。

相比之下,直接给value属性赋值可以避免这种开销。因为value属性是一个可变的变量,我们可以直接修改它的值,而不需要创建新的对象。这样做不仅可以提高程序的性能,还可以避免因为频繁创建对象而导致的内存泄漏等问题。

另外,需要注意的是,emittryEmit方法是异步的,它们会将新的状态值放入一个队列中,等待下一次事件循环时再进行处理。而直接给value属性赋值是同步的,它会立即更新状态值,并通知所有的观察者。因此,在某些情况下,直接给value属性赋值可能更加方便和可靠。

综上所述,虽然StateFlow提供了多种方式来更新状态值,但是推荐使用直接给value属性赋值的方式。这样做可以避免内存分配和垃圾回收的开销,提高程序的性能,同时也更加方便和可靠。

(4) distinctUntilChanged无效

对于普通的flow来说distinctUntilChanged是生效的(见上面二、9、distinctUntilChanged操作符),但是对于StateFlow来说因为本身就会对比前后的值,所以不建议在StateFlow中使用这个操作符。

(5) 如果不想处理默认值可以怎么办

提前订阅(会收到默认值)的前提下,使用drop操作符

val mutableStateFlow = MutableStateFlow(0)

//提前订阅
launch {
    mutableStateFlow.drop(1).collect {
        printMsg("collect $it")
    }
}

(6) emit和tryEmit的区别?

emit 函数是一个挂起函数,它可以在流中发射一个元素。它将暂停当前协程的执行,直到所有已订阅该流的收集器都成功接收到这个元素,然后该协程才会继续执行。如果在发射元素时发生异常,则该异常会向上抛出,并且该流的所有收集器都将失败。

tryEmit 函数也是用于发射元素的函数,但与 emit 不同,它不是一个挂起函数。相反,它会立即返回一个布尔值,指示元素是否已成功发射。如果已成功发射元素,则 tryEmit 函数返回 true;如果已有收集器取消了订阅,或者流已经被终止,则 tryEmit 函数返回 false。

因此,emit 和 tryEmit 的主要区别在于,emit 函数是一个挂起函数,必须等待所有订阅者接收到元素后才会返回,而 tryEmit 函数是一个非挂起函数,会立即返回一个布尔值来指示元素是否已成功发射。在使用时,需要根据具体的情况来选择使用哪个函数。

五、SharedFlow

1、SharedFlow,MutableSharedFlow

看一下MutableSharedFlow方法的源码

@Suppress("FunctionName", "UNCHECKED_CAST")
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
  ...略...
}

MutableSharedFlow的构造函数包含三个参数,分别是replayextraBufferCapacityonBufferOverflow

(1) replay参数表示是否需要缓存最新的元素值,并在新的观察者加入时自动发送。如果该参数为0,则不会缓存最新的元素值;如果该参数为正整数n,则会缓存最新的n个元素值,并在新的观察者加入时自动发送这些元素值。该参数的默认值为0。

(2)extraBufferCapacity参数表示缓冲区的额外容量。如果该参数为0,则缓冲区的容量与replay参数指定的容量相同;如果该参数为正整数n,则缓冲区的容量为replay+n。该参数的默认值为0。

(3)onBufferOverflow参数表示当缓冲区溢出时的处理方式。如果缓冲区已满,而又有新的元素要发射时,就会发生缓冲区溢出。该参数可以是以下三种值之一:

  • BufferOverflow.SUSPEND:挂起当前协程,直到缓冲区有空间可以存储新的元素。
  • BufferOverflow.DROP_OLDEST:丢弃最旧的元素,以腾出空间存储新的元素。
  • BufferOverflow.DROP_LATEST:丢弃最新的元素,不将其存储到缓冲区中。

如果不指定该参数,则默认为BufferOverflow.SUSPEND

2、shareIn

ShareIn方法的源码为:

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T> {
    ...略...
}

三个参数的含义分别为:

(1) 参数一:CoroutineScope

指定了共享流的作用域,即共享流将在哪个协程范围内执行。这个参数的值应该是一个CoroutineScope类型的对象。

在Kotlin协程中,CoroutineScope代表了一个协程的上下文和生命周期,它可以用来启动协程,或者用来取消一个协程以及其子协程。

在使用shareIn创建共享流时,你可以传递一个协程作用域,表示共享流将在该作用域内执行。这样,当协程作用域被取消时,共享流也将被取消,从而避免资源泄漏。

(2) 参数二:SharingStarted

SharingStarted 具有以下几个选项:

  • WhileSubscribed:默认选项,表示当至少有一个订阅者时才启动共享热流。也就是说,在第一个订阅者订阅共享热流之前,共享热流是不会启动的,而在最后一个订阅者取消订阅后,共享热流会自动停止。

    传入时间(毫秒值)的意义是什么?

     public fun WhileSubscribed(
        stopTimeoutMillis: Long = 0,   //毫秒值
        replayExpirationMillis: Long = Long.MAX_VALUE
     ): SharingStarted = StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)
    

    共享流在没有订阅者时最长的存活时间,如果在此期间没有订阅者,共享流将自动关闭。

  • Eagerly:表示在调用 shareIn 时立即启动共享热流,无论是否有订阅者。这意味着即使没有订阅者,共享热流也会一直运行,直到手动取消。

  • Lazily:表示在第一个订阅者订阅共享热流时才启动共享热流,而在最后一个订阅者取消订阅后,共享热流会自动停止。与 WhileSubscribed 相比,Lazily 选项会在第一个订阅者订阅之前等待一段时间,因此可以避免在没有订阅者的情况下浪费资源。

  • Started:表示在调用 shareIn 时立即启动共享热流,并且不会自动停止,直到手动取消。

(3) 参数三:replay

如果有新的订阅者订阅,返回缓存数据的最新数据的个数。

(4) shareIn的优势在哪里?

  • 节省内存和计算资源:当多个观察者订阅同一数据流时,使用 shareIn 可以避免为每个观察者创建新的数据流。相反,所有观察者都可以共享同一数据流。这可以大大减少内存和计算资源的使用,特别是在订阅者数量很大的情况下。

  • 提高性能和响应性:当多个观察者共享同一数据流时,数据只需要被计算和分发一次,而不需要为每个观察者计算和分发一次。这可以减少计算时间和延迟,并提高应用程序的响应性。

  • 简化代码:使用 shareIn 可以使代码更简单和更易于维护。它可以避免重复代码,使数据共享和订阅更加直观和易于理解。

总之,shareIn 函数可以提高应用程序的性能和响应性,减少内存和计算资源的使用,并简化代码。这使得它在 Android 开发中非常有用,尤其是在需要共享数据的情况下。

(5) shareIn使用场景

操作同一份数据源,需要使用数据时将冷流转化为热流

//同一份数据源
val flow = flow {
    for (i in 1..3) {
        delay(1000)
        emit(i)
        printMsg("emit $i")
    }
}
//Flow是冷流不会主动发射数据,转换成热流才会主动发射数据。这样在需要的时候再转换,可以节约资源。
val shareInFlow =
    flow.shareIn(lifecycleScope, SharingStarted.WhileSubscribed(5000), 1)


// 在协程中收集数据。Activity关闭,lifecycleScope作用域的协程终止,热流也会终止
lifecycleScope.launchWhenCreated {
    shareInFlow.collect {
        printMsg("collect before $it")
    }
}

lifecycleScope.launchWhenCreated {
    shareInFlow.collect {
        printMsg("collect after $it")
    }
}

//日志
内容:collect before 1 线程:main @coroutine#6
内容:collect after 1 线程:main @coroutine#7
内容:emit 1 线程:main @coroutine#9
内容:collect before 2 线程:main @coroutine#6
内容:collect after 2 线程:main @coroutine#7
内容:emit 2 线程:main @coroutine#9
内容:collect before 3 线程:main @coroutine#6
内容:collect after 3 线程:main @coroutine#7
内容:emit 3 线程:main @coroutine#9

更实用一点的场景: 比如做蓝牙开发,很多时候,我们会将蓝牙设备给App的数据处理后直接发射出去,不管页面需不需要,就一直发,这会浪费一定的资源,我们可以把蓝牙的数据包装成冷流,需要的时候转换为热流,下面是一个简陋的例子:

class MainActivity : AppCompatActivity() {

    private var mBleData = ""
    private var flow: Flow<String>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //打印协程名称
        System.setProperty("kotlinx.coroutines.debug", "on")

        //发射蓝牙的数据
        emitBleData()

        //需要使用数据时将冷流转化成热流
        val shareInFlow =
            flow!!.shareIn(lifecycleScope, SharingStarted.WhileSubscribed(5000), 1)
        //如果不想收到的数据重复,可以通过stateIn转换成StateFlow
        // flow!!.stateIn(lifecycleScope, SharingStarted.WhileSubscribed(5000), 1)


        // 需要数据时订阅
        lifecycleScope.launchWhenCreated {
            shareInFlow.collect {
                printMsg("collect: $it")
            }
        }
    }

    private fun emitBleData() {
        flow = flow {
            while (true) {
                //假设蓝牙数据3秒发射一次,这里也延迟3秒
                delay(3000)
                //收到的数据往往不在这里,但是我们可以把它定义为成员变量,然后把新的成员变量发射出来
                mBleData = getBleData()
                emit(mBleData)
                printMsg("emit: $mBleData $flow")
            }
        }
    }

    //模拟蓝牙数据: 随机产生数据
    private fun getBleData(): String = "ble data ${Random.nextInt(100)}"
}

3、使用场景

  • 从一个单一数据源共享数据

  • 如果将参数定义为BufferOverflow.DROP_OLDEST(丢弃旧数据),MutableSharedFlow可以当做一个固定长度的缓冲区使用。

  • SharedFlow在共享的ViewModel中共享数据。(这里涉及一个知识点如何在项目中设计一个全局可用的ViewModel对象,这样都能访问同一份数据源?)

4、SharedFlow中的数据是如何移动的?

image.png

通常情况下,当调用emit方法发射数据时,如果缓存数组的buffered values未达到最大容量,则发射的数据将保存到缓存中,并立即返回emit方法。如果缓存数组的buffered values已达到最大容量,则调用emit方法的协程会被立即挂起,并且它的续体和数据会被封装成一个Emitter类型的对象,保存到缓存数组的queued emitters中。

当buffered values中位置为0的数据被所有的订阅者都处理后,buffered values会前移动一位。这时,queued emitters中位置为7的Emitter类型的对象就会被“拆箱”,将其中保存的数据存放到位置7,同时恢复其中保存的emit方法所在续体的执行。之后,位置7将作为buffered values的一部分。

参考了以下内容:

为什么说Flow是冷的?

Kotlin协程:MutableSharedFlow的实现原理

学习笔记