StateFlow与SharedFlow如何取舍?

StateFlow和SharedFlow 都是热流,它们可以在多个收集器之间共享数据。本文主要就以下个方面进行讲解:

一、基本使用

二、如何取舍

三、二者的关系

一、基本使用

1.0引入相关配置(可跳过)

libs.versions.toml

ini 复制代码
[versions]


lifecycleRuntimeKtx = "2.6.1"

[libraries]

androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }

app build.gradle.kts

scss 复制代码
implementation(libs.androidx.lifecycle.viewmodel.compose)

1.1 StateFlow的基本使用 StateFlow的基本使用方式和LiveData类似,甚至可以说StateFlow就是LiveData的平替。

ViewModel

kotlin 复制代码
class FlowViewModel:ViewModel() {

    private val _currentTime= MutableStateFlow<String>("")
    val currentTime:StateFlow<String> = _currentTime.asStateFlow()

    fun getCurrentTime(){
        val format=SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
        val date= Date()
        val time=format.format(date)
        _currentTime.value = "当前时间:${time}"
    }

}

MainActivity

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ShowCurrentTime()
        }
    }
}

@Composable
fun ShowCurrentTime(modifier: Modifier = Modifier) {
    val viewModel: FlowViewModel = viewModel()
    val timeState by viewModel.currentTime.collectAsState()
    Column(modifier.padding(top = 30.dp, start = 10.dp)) {
        Button(onClick = {
            viewModel.getCurrentTime()
        }) {
            Text("点击获取时间")
        }

        Text(text = timeState)
    }
}

上面的代码非常简单,首先在ViewModel中创建了一个私有的MutableStateFlow,然后再创建了一个可访问的StateFlow,这一点和LiveData是不是很相似,最后再创建了个getCurrentTime方法;在MainActivity中 通过ShowCurrentTime方法来显示当前UI,其中点击Button就可以调用viewModel中的getCurrentTime方法。 其中最终要的就是 viewModel.currentTime.collectAsState()。

1.2 SharedFlow的基本使用

kotlin 复制代码
private val _currentTimeShareFlow=MutableSharedFlow<String>()
val currentTimeShareFlow:SharedFlow<String> = _currentTimeShareFlow.asSharedFlow()


fun getCurrentTimeShareFlow(){
    val format=SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    val date= Date()
    val time=format.format(date)
    viewModelScope.launch {
        _currentTimeShareFlow.emit("当前时间:${time}")
    }

}

SharedFlow的发射端基本和StateFlow一致,但有两点不同1、它不需要初始值,2、发射数据需要手动调用,并且需要在协程作用域中调用;SharedFlow的接收端有2种方式接收。

方式一、通过LaunchedEffect启动协程作用域,不修改发射端的代码,因为sharedFlow通过emit方法发射出来,所以我们可以通过collect来收集,具体代码如下:

scss 复制代码
@Composable
fun ShowCurrentTimeShareFlow(modifier: Modifier = Modifier) {
    val viewModel: FlowViewModel = viewModel()
    var currentTime by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        viewModel.currentTimeShareFlow.collect{
            currentTime=it
        }
    }
    Column(modifier.padding(top = 30.dp, start = 10.dp)) {
        Button(onClick = {
            viewModel.getCurrentTimeShareFlow()
        }) {
            Text("点击获取时间")
        }
        Text(text = currentTime)
    }
}

上面的代码新增了一行var currentTime by remember { mutableStateOf("") } 因为我们在collect方法中收集了数据,但是collect不能调用Compose方法,因此不能直接在collect方法中使用Text(it) 所以新增了一个成员变量。运行结果也是一模一样。

方式二、发射端和接收端都需要修改代码,具体如下:

arduino 复制代码
private val _currentTimeShareFlow=MutableSharedFlow<String>(replay = 1)
val currentTimeShareFlow:SharedFlow<String> = _currentTimeShareFlow.asSharedFlow()
ini 复制代码
@Composable
fun ShowCurrentTimeShareFlow2(modifier: Modifier = Modifier) {
    val viewModel: FlowViewModel = viewModel()
    val currentTime by viewModel.currentTimeShareFlow.collectAsState(initial = "2013-06-05")

    Column(modifier.padding(top = 30.dp, start = 10.dp)) {
        Button(onClick = {
            viewModel.getCurrentTimeShareFlow()
        }) {
            Text("点击获取时间")
        }
        Text(text = currentTime)
    }
}

上述代码中发射端新增了一个replay = 1 的参数,接收端也新增了一个initial = "2013-06-05" 的参数,接收端的这个很好理解,就是初始值,程序一运行上来,ShareFlow没有发射值的情况下,就是用这个initial的值;

那replay=1是什么意思呢?可以去掉吗?

先回答第二个问题,这里可以去掉,我们去掉replay = 1程序依然能正常运行,这里之所以加上,主要是提前引出第二大介绍内容(稍后再说)。

那replay=1究竟是什么意思呢?它的意思是缓存最近发射的1个值,新的订阅者会立即收到这个值。避免状态不一致的问题。

例如当屏幕旋转时UI会重建,但是我们设置了replay=1界面上显示的还是最新数据。

移除replay=1,我们会发现随着屏幕的旋转,界面上的最新数据在不断变动。

二、如何取舍

先说说什么是粘性,StateFlow 和 ShareFlow 的本质都是基于发送者观察者设计模式来设计的,如果观察者在未观察之前,发送者已经发送了数据,当观察者开始观察时,要不要收到刚刚发送者发送的数据。如果还能收到刚才的数据这种行为就叫做粘性,如果不能收到那么就叫非粘性。这两种行为需要到具体的业务中具体判断具体分析。

2.1 StateFlow是粘性的

举个例子viewModel中的代码不动,修改Actiivty中的代码

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Column {
                val viewModel: FlowViewModel = viewModel()

                var isEnter by remember { mutableStateOf(false) }

                ShowCurrentTime{
                    isEnter =true
                }
                if(isEnter){
                    SystemClock.sleep(100)
                    val time=Date().time
                    val currentTime by viewModel.currentTime.collectAsState()
                    Text(text ="$currentTime---${time}")
                }
            }

        }
    }
}
scss 复制代码
@Composable
fun ShowCurrentTime(modifier: Modifier = Modifier,onClick:()->Unit) {
    val viewModel: FlowViewModel = viewModel()
    val timeState by viewModel.currentTime.collectAsState()
    var number by remember { mutableStateOf(0) }

    Column(modifier.padding(top = 30.dp, start = 10.dp)) {
        Button(onClick = {
            viewModel.getCurrentTime()

            if(number==1){
                onClick.invoke()
            }
            number++
        }) {
            Text("点击获取时间")
        }
        val time=Date().time
        if(!timeState.isNullOrEmpty()){
            Text(text = "${timeState}---${time}")
        }

    }
}

上面的代码很简单,在ShowCurrentTime方法中,一上来就开始观察currentTime,然后点击Button会调用viewModel中的getCurrentTime()方法,再然后会触发ShowCurrentTime方法进行重组,将时间显示在Text上。当第2次点击Button的时候,会执行onClick回调,接着setContent方法中触发重组,休眠100毫秒后才开始观察currentTime。

如果两个Text的数据一值,则说明StateFlow是粘性的,因为setContent中的Text是在先发送,并且休眠了100毫秒后再观察的。

可以看到两个Text前面的值是一模一样,后面的毫秒值也是第二个比第一个大了100毫秒。所以StateFlow 是粘性的。

2.2SharedFlow是非粘性的

举个例子: 修改viewModel中的代码:

arduino 复制代码
private val _currentTimeShareFlow=MutableSharedFlow<String>()
val currentTimeShareFlow:SharedFlow<String> = _currentTimeShareFlow.asSharedFlow()

修改Activity中的代码

scss 复制代码
@Composable
fun ShowCurrentTimeShareFlow2(modifier: Modifier = Modifier,onClick:(Int)->Unit) {
    val viewModel: FlowViewModel = viewModel()
    val currentTime by viewModel.currentTimeShareFlow.collectAsState(initial = "2013-06-05")

    var number by remember { mutableStateOf(0) }
    Column(modifier.padding(top = 30.dp, start = 10.dp)) {
        Button(onClick = {
            viewModel.getCurrentTimeShareFlow()
            number++
            if(number==1){
                onClick.invoke(1)
            }

        }) {
            Text("点击获取时间")
        }
        val time=Date().time
        if(!currentTime.isNullOrEmpty()){
            Text(text = "${currentTime}---${time}")
        }
    }
}
kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Column {
                val viewModel: FlowViewModel = viewModel()

                var isEnter by remember { mutableStateOf(false) }

                ShowCurrentTimeShareFlow2{
                    isEnter =true
                }
                if(isEnter){
                    SystemClock.sleep(100)
                    val time=Date().time
                    val currentTime by viewModel.currentTimeShareFlow.collectAsState(initial = "2013-06-05")
                    Text(text ="$currentTime---${time}")
                }
            }

        }
    }
}

上面代码的逻辑和之前的StateFlow差不多,就不做过多的解释了,我们看同样的方式,第二个Text在延迟100毫秒后再观察数据源,就和第一个有差别了,他们的数据不统一,也就坐实了ShareFlow是非粘性的。

细心的同学已经发现了,我们将viewModel中的shareFlow 中的 replay = 1去掉了,如果加上会是什么效果? 修改代码

arduino 复制代码
private val _currentTimeShareFlow=MutableSharedFlow<String>(replay = 1)
val currentTimeShareFlow:SharedFlow<String> = _currentTimeShareFlow.asSharedFlow()

WTF 居然实现了StateFlow一模一样的效果,居然也可以是粘性的。

先抛出问题,这一点我们后面再说。

粘性是选择使用StateFlow 和使用SharedFlow的一个参考,另外一个重要的参考是 StateFlow表示共享1个单一的,随时间演变的,当前最新状态;SharedFlow是广播给0个或者多个接受者,事假是离散的,一次性的。

所以我们表示界面状态的时候就用StateFlow,如果有事件处理就选SharedFlow。

例如 StateFlow的核心是"始终持有可被观察的当下",就像手机电量显示------你任何时候看屏幕都必须看到当前真实电量;而SharedFlow更像是微信消息,新消息来了就推送,但不会总把三个月前的聊天记录塞给你。

三、二者的关系

前面我们通过修改SharedFlow中的replay=1,就达成了和StateFlow一样的粘性效果,隐隐预约感觉二者之间有一定的联系,先看看源码.

kotlin 复制代码
public interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> {
    /**
     * The current value of this state flow.
     *
     * Setting a value that is [equal][Any.equals] to the previous one does nothing.
     *
     * This property is **thread-safe** and can be safely updated from concurrent coroutines without
     * external synchronization.
     */
    public override var value: T

    /**
     * Atomically compares the current [value] with [expect] and sets it to [update] if it is equal to [expect].
     * The result is `true` if the [value] was set to [update] and `false` otherwise.
     *
     * This function use a regular comparison using [Any.equals]. If both [expect] and [update] are equal to the
     * current [value], this function returns `true`, but it does not actually change the reference that is
     * stored in the [value].
     *
     * This method is **thread-safe** and can be safely invoked from concurrent coroutines without
     * external synchronization.
     */
    public fun compareAndSet(expect: T, update: T): Boolean
}

原来StateFlow 是SharedFlow的一个子类啊!

既然SharedFlow能实现StateFlow的效果那为什么还需要单独设计StateFlow呢?

主要原因在于StateFlow不是为了能做SharedFlow能做的事情,而是为了在状态管理领域,提供一种更优化、更简洁、更符合特定语义的专业工具。

举个不太恰当的例子:螺丝刀能不能当锤子用,偶尔用上两次问题不大,但是不能一直把螺丝刀当锤子用啊。

对比StateFlow和sharedFlow不难发现,StateFlow强制要求初始值,保证任何时候都有状态可查,SharedFlow则没有;另外StateFlow.update方法提供了一种原子化的、基于旧值计算新值的便捷方式,非常适合状态更新逻辑。SharedFlow 没有这个。

总结:SharedFlow 是强大的事件广播工具。StateFlow 是专门为状态管理优化的工具

相关推荐
小阳睡不醒26 分钟前
小白成长之路-部署Zabbix7(二)
android·运维
mmoyula2 小时前
【RK3568 PWM 子系统(SG90)驱动开发详解】
android·linux·驱动开发
你过来啊你4 小时前
Android用户鉴权实现方案深度分析
android·鉴权
kerli7 小时前
Android 嵌套滑动设计思想
android·客户端
恣艺8 小时前
LeetCode 854:相似度为 K 的字符串
android·算法·leetcode
阿华的代码王国8 小时前
【Android】相对布局应用-登录界面
android·xml·java
QmDeve9 小时前
原生Android Java调用系统指纹识别方法
android