熱流(Hot Flow)和冷流(Cold Flow)是 Kotlin 協程中 Flow
的兩種不同行為模式,它們在數據發射、訂閱機制和使用場景上有顯著區別。以下是詳細解析:
1. 冷流(Cold Flow)
定義
冷流是按需觸發的:只有被收集(
collect
)時才會開始發射數據。每個收集者獨立觸發流:每個新的訂閱者都會觸發一次完整的數據流執行。
類似“單次數據流”:每次訂閱都會重新執行流的邏輯(如網絡請求、數據庫查詢)。
特點
數據獨立性:每個收集者獲取的數據流是獨立的。
生命周期綁定收集者:流的執行隨收集者的協程作用域取消而終止。
代表實現
通過
flow { ... }
構建的標準Flow
。由
asFlow()
、flowOf()
等創建的流。
使用場景
一次性數據操作:如網絡請求、數據庫查詢。
需要按需觸發的任務:例如用戶手動刷新數據。
代碼示例
// 冷流示例:每次收集都會重新執行
fun fetchDataCold(): Flow<String> = flow {
println("冷流:開始執行網絡請求")
delay(1000) // 模擬耗時操作
emit("數據結果")
}
// 收集兩次,觸發兩次獨立的流執行
viewModelScope.launch {
fetchDataCold().collect { println("收集結果1: $it") } // 輸出結果
fetchDataCold().collect { println("收集結果2: $it") } // 再次執行并輸出
}
輸出:
冷流:開始執行網絡請求
收集結果1: 數據結果
冷流:開始執行網絡請求
收集結果2: 數據結果
2. 熱流(Hot Flow)
定義
熱流是主動觸發的:無論是否有收集者訂閱,數據都會持續發射。
多訂閱者共享數據:所有收集者共享同一個數據流,后續訂閱者可能錯過之前的數據。
類似“廣播”:數據發射獨立于收集者的生命周期。
特點
數據共享性:多個收集者共享同一數據源。
生命周期獨立:流可能持續運行,需要手動管理資源(如取消)。
代表實現
StateFlow
:始終保存最新狀態,新訂閱者立即獲得當前值。SharedFlow
:可配置歷史數據緩存(如replay
),支持多訂閱者。通過
Channel
轉換的流:如consumeAsFlow()
。
使用場景
實時狀態管理:如 UI 狀態、用戶位置更新。
事件廣播:如全局通知、實時消息推送。
代碼示例
// 熱流示例:SharedFlow
private val _sharedData = MutableSharedFlow<String>(replay = 1) // 緩存最近1個值
val sharedData: SharedFlow<String> = _sharedData
// 主動發射數據(不依賴收集者)
fun startEmitting() {
viewModelScope.launch {
repeat(3) {
delay(1000)
_sharedData.emit("數據$it")
println("熱流:發射數據$it")
}
}
}
// 收集示例
viewModelScope.launch {
// 延遲訂閱,錯過前兩次發射
delay(2500)
sharedData.collect { println("收集結果: $it") }
}
輸出:
熱流:發射數據0
熱流:發射數據1
熱流:發射數據2
收集結果: 數據1 // 因 replay=1,收到最后一次緩存的 "數據1"
收集結果: 數據2 // 后續實時數據
3. 核心區別對比
特性 | 冷流(Cold Flow) | 熱流(Hot Flow) |
---|---|---|
觸發時機 | 按需觸發(收集時啟動) | 主動觸發(獨立于收集者) |
數據共享性 | 每個收集者獨立執行流 | 多收集者共享同一數據源 |
歷史數據 | 每次訂閱從頭開始 | 可配置緩存(如 replay ) |
資源管理 | 自動隨收集者取消釋放 | 需手動管理(如取消協程) |
典型使用場景 | 網絡請求、數據庫查詢 | 實時狀態、事件廣播 |
代表實現 |
flow { ... } 、asFlow()
|
StateFlow 、SharedFlow
|
4. 使用場景代碼對比
冷流場景:每次收集觸發獨立請求
// 冷流:每次收集重新執行數據庫查詢
fun getUserByIdCold(id: Int): Flow<User> = flow {
val user = database.queryUser(id) // 每次收集觸發查詢
emit(user)
}
// 兩個界面分別收集同一流
viewModelScope.launch {
getUserByIdCold(1).collect { println("界面1: $it") }
}
viewModelScope.launch {
getUserByIdCold(1).collect { println("界面2: $it") }
}
// 輸出兩次數據庫查詢日志
熱流場景:共享實時狀態
// 熱流:StateFlow 實時更新用戶狀態
private val _userState = MutableStateFlow<User?>(null)
val userState: StateFlow<User?> = _userState.asStateFlow()
// 更新狀態
fun updateUser(user: User) {
_userState.value = user
}
// 多個界面觀察同一狀態
viewModelScope.launch {
userState.collect { println("界面1: $it") }
}
viewModelScope.launch {
userState.collect { println("界面2: $it") }
}
// 所有界面實時收到相同狀態更新
5. 總結
冷流:適用于按需觸發、數據獨立的場景(如網絡請求),每次訂閱重新執行邏輯。
熱流:適用于主動推送、數據共享的場景(如 UI 狀態管理),多個訂閱者共享同一數據源。
根據業務需求選擇合適的數據流類型,可以顯著提升代碼的效率和可維護性。
熱流與冷流的深入解析
1. 熱流與冷流的轉換技巧
在實際開發中,常需要將冷流轉換為熱流,以共享數據源或保持狀態持久化。以下是常見的轉換方法:
使用 stateIn
將冷流轉為 StateFlow
class MyViewModel(repository: MyRepository) : ViewModel() {
// 將冷流轉換為熱流 StateFlow
val data: StateFlow<Result<String>> = repository.fetchDataCold()
.map { Result.Success(it) }
.catch { emit(Result.Error(it)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 5秒無訂閱者后停止
initialValue = Result.Loading
)
}
// Repository 返回冷流
class MyRepository {
fun fetchDataCold(): Flow<String> = flow {
emit("數據")
delay(1000)
}
}
作用:確保多個UI組件訂閱同一數據源時,不會重復觸發網絡請求。
-
參數說明:
-
started
:控制流的共享策略。WhileSubscribed
表示無訂閱者時自動取消上游流,避免資源浪費。
-
使用 shareIn
轉為 SharedFlow
val hotFlow: SharedFlow<String> = coldFlow
.shareIn(
scope = viewModelScope,
started = SharingStarted.Eagerly, // 立即啟動,不等待訂閱者
replay = 1 // 新訂閱者收到最近1個值
)
2. 如何選擇熱流與冷流?
根據場景需求做出決策:
-
選擇冷流的情況:
數據需按需觸發:如點擊按鈕后加載數據。
獨立數據副本:每個訂閱者需要完整的數據流(如日志記錄)。
資源敏感操作:如大文件下載,確保無訂閱時立即釋放資源。
-
選擇熱流的情況:
狀態持久化:如用戶登錄狀態,需跨界面共享。
實時事件推送:如WebSocket消息、傳感器數據。
避免重復計算:多個訂閱者共享同一計算結果。
3. SharedFlow vs StateFlow:核心區別
兩者均為熱流,但適用場景不同:
特性 | SharedFlow | StateFlow |
---|---|---|
數據緩存 | 可配置 replay (緩存多個歷史值) |
僅緩存最新值(等效 replay=1 ) |
初始值 | 不需要初始值 | 必須提供初始值 |
適用場景 | 事件通知(如按鈕點擊) | 狀態管理(如UI控件的顯示/隱藏) |
訂閱者接收數據 | 新訂閱者收到 replay 緩存的歷史值 |
立即收到最新值 |
SharedFlow 示例:事件通知
// ViewModel 中定義事件流
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events
fun onButtonClick() {
viewModelScope.launch {
_events.emit(Event.ShowToast("點擊成功"))
}
}
// Activity 收集事件
lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
is Event.ShowToast -> showToast(event.message)
}
}
}
StateFlow 示例:UI狀態管理
private val _loadingState = MutableStateFlow(false)
val loadingState: StateFlow<Boolean> = _loadingState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_loadingState.value = true
// 模擬加載數據
delay(1000)
_loadingState.value = false
}
}
4. 常見陷阱與解決方案
陷阱1:冷流重復觸發網絡請求
-
問題代碼:
// 每次 collect 都會觸發新請求 fun loadData() = flow { fetchDataFromNetwork() }
-
修復方案:
使用
stateIn
或shareIn
轉為熱流,確保數據共享。
陷阱2:未正確處理生命周期導致內存泄漏
-
錯誤示例:
// 在 Activity 中直接收集,未綁定生命周期 viewModel.data.collect { ... } // 可能導致后臺繼續收集
-
正確做法:
使用
repeatOnLifecycle
確保只在活躍狀態收集:lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.data.collect { ... } } }
陷阱3:SharedFlow 事件被多次處理
問題:多個訂閱者導致事件重復消費。
解決:使用
SharedFlow(extraBufferCapacity=0)
或單訂閱者模式。
5. 完整場景代碼示例
場景:冷流轉熱流 + 狀態管理
// Repository 提供冷流
class DataRepository {
fun fetchData(): Flow<String> = flow {
delay(1000)
emit("新數據")
}
}
// ViewModel 轉換為熱流
class MyViewModel(repository: DataRepository) : ViewModel() {
private val _data = repository.fetchData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = "初始值"
)
val data: StateFlow<String> = _data
}
// Activity 安全收集
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { data ->
updateUI(data)
}
}
}
}
}
總結
冷流:輕量級、按需觸發,適合獨立數據操作。
熱流:持久化、共享數據源,適合狀態管理和實時事件。
-
關鍵技巧:
使用
stateIn
/shareIn
優化冷流為熱流。通過
repeatOnLifecycle
確保安全收集。根據事件(SharedFlow)或狀態(StateFlow)選擇合適的熱流類型。
掌握兩者的區別與應用場景,能顯著提升代碼的可維護性和性能表現。