在 Jetpack Compose 中,Kotlin Flow 是处理异步数据流的核心工具,而 SharedFlow
和 StateFlow
是最常用的两种 Flow 类型。但很多开发者对它们的适用场景、如何与 LaunchedEffect
配合使用存在困惑。本文将深入探讨它们的区别,并给出最佳实践。
1. SharedFlow vs StateFlow:事件 vs 状态
(1) SharedFlow:用于一次性事件
SharedFlow
是一个 热流(Hot Flow),适合表示 事件(Events),例如:
- 显示 Toast/弹窗
- 导航请求(Navigation)
- 按钮点击后的瞬时反馈
特点:
- 无初始值:默认不会存储数据,新订阅者不会立即收到旧值(除非配置
replay
)。 - 多订阅者支持:多个收集者可以独立消费事件。
- 适合瞬时操作:事件被消费后不会再次触发。
示例:
class MyViewModel : ViewModel() {private val _toastEvent = MutableSharedFlow<String>()val toastEvent = _toastEvent.asSharedFlow()fun showToast(message: String) {viewModelScope.launch {_toastEvent.emit(message) // 发送一次性事件}}
}
(2) StateFlow:用于持久状态
StateFlow
是 SharedFlow
的特殊变体,适合表示 状态(State),例如:
- UI 的显示/隐藏状态(如加载中、弹窗是否可见)
- 表单数据(如输入框内容)
- 登录状态(已登录/未登录)
特点:
- 必须有初始值:新订阅者会立即获取当前值。
- 自动去重:如果新值与旧值相同,不会触发更新。
- 适合长期状态:状态会一直保持,直到被修改。
示例:
class MyViewModel : ViewModel() {private val _isLoading = MutableStateFlow(false)val isLoading = _isLoading.asStateFlow()fun fetchData() {viewModelScope.launch {_isLoading.value = true// 加载数据..._isLoading.value = false}}
}
2. 在 Compose 中收集 Flow:LaunchedEffect vs collectAsState
(1) 收集 SharedFlow(使用 LaunchedEffect)
由于 SharedFlow
表示事件,通常使用 LaunchedEffect
监听,确保 只触发一次,并在组件退出时自动取消。
示例:
@Composable
fun MyScreen(viewModel: MyViewModel) {var showToast by remember { mutableStateOf(false) }var toastMessage by remember { mutableStateOf("") }// ✅ 使用 LaunchedEffect 监听事件LaunchedEffect(Unit) {viewModel.toastEvent.collect { message ->toastMessage = messageshowToast = true}}if (showToast) {Toast(message = toastMessage) {showToast = false // 关闭 Toast}}
}
关键点:
LaunchedEffect(Unit)
保证只注册一次。- 协程会在
MyScreen
退出时自动取消,避免内存泄漏。
(2) 收集 StateFlow(使用 collectAsState)
由于 StateFlow
表示状态,Compose 提供了 collectAsState()
扩展函数,可以自动触发重组。
示例:
@Composable
fun MyScreen(viewModel: MyViewModel) {val isLoading by viewModel.isLoading.collectAsState()if (isLoading) {CircularProgressIndicator()} else {Button(onClick = { viewModel.fetchData() }) {Text("加载数据")}}
}
关键点:
collectAsState()
自动将StateFlow
转换为 Compose 的State
。- 状态变化时,UI 自动刷新。
3. 常见问题解答
Q1:为什么 SharedFlow 要用 LaunchedEffect,而不能用 collectAsState?
collectAsState
适用于 状态(StateFlow),而SharedFlow
是 事件流,如果用collectAsState
,可能会:- 重复触发:因为 Compose 会不断重组,导致事件被多次消费。
- 不符合语义:事件应该被消费一次后消失,而
collectAsState
会持续监听。
Q2:LaunchedEffect 和 rememberCoroutineScope 有什么区别?
LaunchedEffect | rememberCoroutineScope | |
---|---|---|
用途 | 用于 副作用(如监听 Flow) | 用于 手动控制协程(如按钮点击) |
生命周期 | 随 Composable 退出自动取消 | 需要手动取消 |
示例 | 监听 SharedFlow 事件 | 在 onClick 中发起网络请求 |
Q3:StateFlow 能不能用 LaunchedEffect 监听?
可以,但不推荐:
// ❌ 能用,但不如 collectAsState 方便
LaunchedEffect(Unit) {viewModel.isLoading.collect { isLoading ->// 需要手动触发重组}
}// ✅ 推荐方式
val isLoading by viewModel.isLoading.collectAsState()
4. 终极选择指南
场景 | 推荐方案 |
---|---|
一次性事件(Toast、弹窗、导航) | SharedFlow + LaunchedEffect |
UI 状态(加载中、数据展示) | StateFlow + collectAsState |
需要手动控制协程(如按钮点击) | rememberCoroutineScope + launch |
5. 总结
SharedFlow
= 事件(一次性) → 用LaunchedEffect
监听。StateFlow
= 状态(持久) → 用collectAsState
自动刷新 UI。- 避免手动
CoroutineScope.launch
监听 Flow,容易导致泄漏或重复订阅。
正确使用 Flow
+ Compose
可以让你的代码更健壮、更符合响应式编程的最佳实践! 🚀