在 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
可以讓你的代碼更健壯、更符合響應式編程的最佳實踐! 🚀