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

相关推荐
叽哥1 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走1 小时前
创建自定义语音录制View
android·前端
用户2018792831671 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831671 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker3 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong3 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil4 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌11 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
书弋江山12 小时前
flutter 跨平台编码库 protobuf 工具使用
android·flutter
来来走走15 小时前
Flutter开发 webview_flutter的基本使用
android·flutter