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 是专门为状态管理优化的工具

相关推荐
阿巴斯甜8 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker9 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952710 小时前
Andorid Google 登录接入文档
android
黄林晴11 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android