一 Kotlin Flow 中的 stateIn 和 shareIn
一、簡單比喻理解
想象一個水龍頭(數據源)和幾個水杯(數據接收者):
- 普通 Flow(冷流):每個水杯來接水時,都要重新打開水龍頭從頭放水
- stateIn/shareIn(熱流):水龍頭一直開著,水存在一個水池里,任何水杯隨時來接都能拿到水
二、stateIn 是什么?
就像手機的狀態欄
- 總是顯示最新的一條信息(有
當前值
) - 新用戶打開手機時,立刻能看到最后一條消息
- 適合用來表示"當前狀態",比如:
- 用戶登錄狀態(已登錄/未登錄)
- 頁面加載狀態(加載中/成功/失敗)
- 實時更新的數據(如股票價格)
代碼示例:
// 創建一個永遠知道當前溫度的溫度計
val currentTemperature = sensorFlow.stateIn(scope = viewModelScope, // 在ViewModel生命周期內有效started = SharingStarted.WhileSubscribed(5000), // 5秒無訂閱就暫停initialValue = 0 // 初始溫度0度)// 在Activity中讀取(總是能拿到當前溫度)
textView.text = "${currentTemperature.value}°C"
三、shareIn 是什么?
就像廣播電臺
- 不保存"當前值"(沒有
.value
屬性) - 新聽眾打開收音機時,可以選擇:
- 從最新的一條新聞開始聽(replay=1)
- 只聽新新聞(replay=0)
- 適合用來處理"事件",比如:
- 顯示Toast提示
- 頁面跳轉指令
- 一次性通知
代碼示例:
// 創建一個消息廣播站
val messages = notificationFlow.shareIn(scope = viewModelScope,started = SharingStarted.Lazily, // 有人收聽時才啟動replay = 1 // 新聽眾能聽到最后1條消息)// 在Activity中收聽廣播
lifecycleScope.launch {messages.collect { msg ->Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()}
}
四、主要區別對比
特性 | stateIn (狀態欄) | shareIn (廣播電臺) |
---|---|---|
有無當前值 | 有(.value 直接訪問) | 無(必須通過collect接收) |
新訂閱者 | 立即獲得最新值 | 可配置獲得最近N條(replay) |
典型用途 | 持續更新的狀態(如用戶積分) | 一次性事件(如"購買成功"提示) |
內存占用 | 始終保存最新值 | 按需緩存(可配置) |
是否熱流 | 是 | 是 |
五、為什么要用它們?
-
節省資源:避免重復計算(多個界面可以共享同一個數據源)
- ? 不用時:每個界面都單獨請求一次網絡數據
- ? 使用后:所有界面共享同一份網絡數據
-
保持一致性:所有訂閱者看到的數據完全相同
- 比如用戶頭像更新后,所有界面立即同步
-
自動管理生命周期:
- 當Activity銷毀時自動停止收集
- 當配置變更(如屏幕旋轉)時保持數據不丟失
六、生活場景類比
場景1:微信群(stateIn)
- 群里最后一條消息就是當前狀態(.value)
- 新成員進群立刻能看到最后一條消息
- 適合:工作群的狀態同步
場景2:電臺廣播(shareIn)
- 主播不斷發送新消息
- 聽眾打開收音機時:
- 可以設置是否聽之前的回放(replay)
- 但無法直接問"剛才最后一首歌是什么"(無.value)
- 適合:交通路況實時播報
七、什么時候用哪個?
用 stateIn 當:
- 需要隨時知道"當前值"
- 數據會持續變化且需要被多個地方使用
- 例如:
- 用戶登錄狀態
- 購物車商品數量
- 實時位置更新
用 shareIn 當:
- 只關心新事件,不關心歷史值
- 事件可能被多個接收者處理
- 例如:
- "訂單支付成功"通知
- 錯誤提示消息
- 頁面跳轉指令
八、超簡單選擇流程圖
要管理持續變化的狀態嗎?是 → 需要直接訪問當前值嗎?是 → 用 stateIn否 → 用 shareIn(replay=1)否 → 這是一次性事件嗎?是 → 用 shareIn(replay=0)
記住這個簡單的口訣:
“狀態用state,事件用share,想要回放加replay”
二 Kotlin Flow 的 shareIn
和 stateIn
操作符完全指南
在 Kotlin Flow 的使用中,shareIn
和 stateIn
是兩個關鍵的操作符,用于優化流的共享和狀態管理。本教程將深入解析這兩個操作符的使用場景、區別和最佳實踐。
一、核心概念解析
1. 冷流 vs 熱流
- 冷流 (Cold Flow):每個收集者都會觸發獨立的執行(如普通的
flow{}
構建器) - 熱流 (Hot Flow):數據發射獨立于收集者存在(如
StateFlow
、SharedFlow
)
2. 為什么需要 shareIn
/stateIn
?
- 避免對上游冷流進行重復計算
- 多個收集者共享同一個數據源
- 將冷流轉換為熱流以提高效率
二、stateIn
操作符詳解
基本用法
val sharedFlow: StateFlow<Int> = flow {// 模擬耗時操作emit(repository.fetchData())
}.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = 0
)
參數說明:
- scope:共享流的協程作用域(通常用
viewModelScope
) - started:共享啟動策略(后文詳細講解)
- initialValue:必須提供的初始值
特點:
- 總是有當前值(通過
value
屬性訪問) - 新收集者立即獲得最新值
- 適合表示 UI 狀態
使用場景示例:
用戶個人信息狀態管理
class UserViewModel : ViewModel() {private val _userState = repository.userUpdates() // Flow<User>.map { it.toUiState() }.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = UserState.Loading)val userState: StateFlow<UserState> = _userState
}
三、shareIn
操作符詳解
基本用法
val sharedFlow: SharedFlow<Int> = flow {emit(repository.fetchData())
}.shareIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),replay = 1
)
參數說明:
- replay:新收集者接收的舊值數量
- extraBufferCapacity:超出 replay 的緩沖大小
- onBufferOverflow:緩沖策略(
SUSPEND
,DROP_OLDEST
,DROP_LATEST
)
特點:
- 可以有多個訂閱者
- 沒有
value
屬性,必須通過收集獲取數據 - 適合事件處理(如 Toast、導航事件)
使用場景示例:
全局事件通知
class EventBus {private val _events = MutableSharedFlow<Event>()val events = _events.asSharedFlow()suspend fun postEvent(event: Event) {_events.emit(event)}// 使用 shareIn 轉換外部流val externalEvents = someExternalFlow.shareIn(scope = CoroutineScope(Dispatchers.IO),started = SharingStarted.Eagerly,replay = 0)
}
四、started
參數深度解析
1. SharingStarted.Eagerly
- 行為:立即啟動,無視是否有收集者
- 用例:需要預先緩存的數據
- 風險:可能造成資源浪費
started = SharingStarted.Eagerly
2. SharingStarted.Lazily
- 行為:在第一個收集者出現時啟動,保持活躍直到 scope 結束
- 用例:長期存在的共享數據
- 注意:可能延遲首次數據獲取
started = SharingStarted.Lazily
3. SharingStarted.WhileSubscribed()
- 行為:
- 有收集者時活躍
- 最后一個收集者消失后保持一段時間(默認 0ms)
- 可配置
stopTimeoutMillis
和replayExpirationMillis
- 用例:大多數 UI 相關狀態
// 保留5秒供可能的重新訂閱
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000)
五、關鍵區別對比
特性 | stateIn | shareIn |
---|---|---|
返回類型 | StateFlow | SharedFlow |
初始值 | 必須提供 | 無要求 |
新收集者獲取 | 立即獲得最新 value | 獲取 replay 數量的舊值 |
值訪問 | 通過 .value 直接訪問 | 必須通過收集獲取 |
典型用途 | UI 狀態管理 | 事件通知/數據廣播 |
背壓處理 | 總是緩存最新值 | 可配置緩沖策略 |
六、最佳實踐指南
1. ViewModel 中的標準模式
class MyViewModel : ViewModel() {// 狀態管理用 stateInval uiState = repository.data.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = null)// 事件處理用 shareInval events = repository.events.shareIn(scope = viewModelScope,started = SharingStarted.Lazily,replay = 1)
}
2. 合理選擇 started
策略
- UI 狀態:
WhileSubscribed(stopTimeoutMillis = 5000)
- 配置變更需保留:
Lazily
- 全局常駐數據:
Eagerly
3. 避免常見錯誤
錯誤1:在每次調用時創建新流
// 錯誤!每次調用都創建新流
fun getUser() = repository.getUserFlow().stateIn(viewModelScope, SharingStarted.Eagerly, null)// 正確:共享同一個流
private val _user = repository.getUserFlow().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val user: StateFlow<User?> = _user
錯誤2:忽略 replay 配置
// 可能丟失事件
.shareIn(scope, SharingStarted.Lazily, replay = 0)// 更安全的配置
.shareIn(scope, SharingStarted.Lazily, replay = 1)
七、高級應用場景
1. 結合 Room 數據庫
@Dao
interface UserDao {@Query("SELECT * FROM user")fun observeUsers(): Flow<List<User>>
}// ViewModel 中
val users = userDao.observeUsers().stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList())
2. 實現自動刷新功能
val autoRefreshData = flow {while(true) {emit(repository.fetchLatest())delay(30_000) // 每30秒刷新}
}.shareIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),replay = 1
)
3. 多源數據合并
val combinedData = combine(repo1.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1),repo2.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
) { data1, data2 ->data1 + data2
}.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList()
)
八、性能優化技巧
-
合理設置 replay:
- UI 狀態:
replay = 1
(確保新訂閱者立即獲得狀態) - 事件通知:
replay = 0
(避免重復處理舊事件)
- UI 狀態:
-
使用 WhileSubscribed 的過期策略:
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000,replayExpirationMillis = 60_000 // 1分鐘后丟棄緩存 )
-
避免過度緩沖:
.shareIn(scope = ...,replay = 1,extraBufferCapacity = 1, // 總共緩沖2個值onBufferOverflow = BufferOverflow.DROP_OLDEST )
九、測試策略
1. 測試 StateFlow
@Test
fun testStateFlow() = runTest {val testScope = TestScope()val flow = flowOf(1, 2, 3)val stateFlow = flow.stateIn(scope = testScope,started = SharingStarted.Eagerly,initialValue = 0)assertEquals(0, stateFlow.value) // 初始值testScope.advanceUntilIdle()assertEquals(3, stateFlow.value) // 最后發射的值
}
2. 測試 SharedFlow
@Test
fun testSharedFlow() = runTest {val testScope = TestScope()val flow = flowOf("A", "B", "C")val sharedFlow = flow.shareIn(scope = testScope,started = SharingStarted.Eagerly,replay = 1)val results = mutableListOf<String>()val job = launch {sharedFlow.collect { results.add(it) }}testScope.advanceUntilIdle()assertEquals(listOf("A", "B", "C"), results)job.cancel()
}
十、總結決策樹
何時使用 stateIn
?
- 需要表示當前狀態(有
.value
屬性) - UI 需要立即訪問最新值
- 適合:頁面狀態、表單數據、加載狀態
何時使用 shareIn
?
- 處理一次性事件
- 需要自定義緩沖策略
- 適合:Toast 消息、導航事件、廣播通知
選擇哪種 started
策略?
WhileSubscribed()
:大多數 UI 場景Lazily
:配置變更需保留數據Eagerly
:需要預加載的全局數據
通過本教程,應該已經掌握了 shareIn
和 stateIn
的核心用法和高級技巧。正確使用這兩個操作符可以顯著提升應用的性能和資源利用率。