一 兩種 SharingStarted
策略的區別:
- SharingStarted.Eagerly:
- 立即開始收集上游流,即使沒有下游訂閱者
- 持續保持活躍狀態,直到 ViewModel 被清除
- 優點:響應更快,數據始終保持最新
- 缺點:消耗更多資源,因為始終在收集數據
- SharingStarted.WhileSubscribed(5000):
- 僅在有下游訂閱者時才開始收集
- 停止收集后等待 5000 毫秒才真正停止上游流
- 優點:更節省資源
- 缺點:首次訂閱時可能有短暫延遲
選擇建議:
// 適合使用 Eagerly 的場景:
// 1. 需要立即獲取和保持數據最新狀態
// 2. 數據更新頻繁且重要的場景
val fragranceChannel = FragranceRepository.observeSelectedChannel().stateIn(viewModelScope, SharingStarted.Eagerly, DEFAULT_CHANNEL)// 適合使用 WhileSubscribed 的場景:
// 1. 數據不需要實時更新
// 2. 想要節省資源的場景
val acStatus = ACStatusRepository.acSwitchStatus.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
兩種策略說明
這是一個典型的Flow訂閱場景。解釋上下游關系:
- 上游(Upstream):
// 在 ViewModel 中
val fragranceChannelMaterialCardView = ToggleButtonState(state = FragranceRepository.observeSelectedChannel() // 這是上游數據源.stateIn(viewModelScope, SharingStarted.Eagerly, DEFAULT_CHANNEL),...
)
- 下游(Downstream):
// 在 Fragment 中
viewModel.fragranceChannelMaterialCardView.state.collect { pos -> // 這是下游訂閱者binding.fragranceSelectedChannel = pos
}
流程說明:
FragranceRepository.observeSelectedChannel()
產生數據.stateIn()
將Flow轉換為StateFlow- Fragment中的
.collect
訂閱這個StateFlow - 當上游數據變化時,下游會收到通知并更新UI
這就像一個管道:
數據源(Repository) -> StateFlow(ViewModel) -> 訂閱者(Fragment)
[上游] [中轉站] [下游]
使用 SharingStarted.Eagerly
意味著即使沒有下游訂閱,上游也會一直產生數據。
如果改用 WhileSubscribed
,只有在Fragment 訂閱時才會開始收集數據。
二 SharingStarted.Eagerly示例
SharingStarted.Eagerly
的收集機制:
class WeatherViewModel : ViewModel() {// 上游數據源 - 模擬溫度傳感器private val temperatureSource = flow {var temp = 20while(true) {emit(temp++)delay(1000)println("上游發射溫度: $temp") // 日志觀察發射}}// 使用 Eagerly 立即開始收集val temperature = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.Eagerly, // 立即開始收集initialValue = 0)init {// 可以在這里觀察收集到的值viewModelScope.launch {temperature.collect { temp ->println("ViewModel 內部收到溫度: $temp")}}}
}// UI層使用
class WeatherFragment : Fragment() {override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 即使這里暫時還沒有收集,上游也在發射數據// 延遲5秒后再開始收集lifecycleScope.launch {delay(5000)viewModel.temperature.collect { temp ->binding.tempText.text = "$temp°C"}}}
}
關鍵點解釋:
-
立即收集的意義:
- 即使沒有下游訂閱者,StateFlow 也會保持最新值
- 當下游開始訂閱時,可以立即獲得最新狀態
- 適合需要持續監控或后臺處理的場景
-
收集過程:
上游發射溫度: 20 (立即開始)
上游發射溫度: 21
上游發射溫度: 22
上游發射溫度: 23
上游發射溫度: 24
[5秒后 Fragment 開始收集]
Fragment收到溫度: 24 (立即獲得最新值)
上游發射溫度: 25
Fragment收到溫度: 25
- 適用場景:
class LocationViewModel : ViewModel() {// 位置追蹤需要持續進行,即使UI暫時不可見val location = locationManager.locationUpdates().stateIn(scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = defaultLocation)
}
- 與 WhileSubscribed 的對比:
// WhileSubscribed - 有訂閱者才收集
val temperature1 = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = 0
)// Eagerly - 立即開始收集
val temperature2 = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = 0
)
- 使用建議:
- 如果數據源消耗資源較大,建議使用
WhileSubscribed
- 如果需要后臺持續處理或保持最新狀態,使用
Eagerly
- 大多數UI狀態場景,推薦使用
WhileSubscribed
class CarViewModel : ViewModel() {// 車輛狀態需要持續監控 - 使用 Eagerlyval carStatus = carMonitor.status.stateIn(scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = CarStatus.Unknown)// UI展示數據 - 使用 WhileSubscribedval uiState = dataSource.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = UiState())
}
所以,Eagerly
的立即收集主要用于:
- 需要持續后臺處理的場景
- 狀態不能中斷的監控場景
- 需要立即響應的關鍵數據流
三 SharingStarted.WhileSubscribed(5000) 示例
下面通過一個具體示例來說明下游停止收集的情況:
class TemperatureViewModel : ViewModel() {// 上游數據源private val temperatureSource = flow {var temp = 20while(true) {emit(temp++)delay(1000) }}// 使用 WhileSubscribed(5000) 轉換為 StateFlowval temperature = temperatureSource.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = 0)
}// Fragment/Activity 中使用
class TemperatureFragment : Fragment() {override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 開始收集 - 這時上游 flow 會開始發射數據viewLifecycleOwner.lifecycleScope.launch {viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.temperature.collect { temp ->binding.tempText.text = "$temp°C"}}}}
}
以下情況會導致下游停止收集:
- Fragment 進入 STOPPED 狀態(如切到后臺):
// repeatOnLifecycle 會在 STOPPED 時自動取消收集
// 但會等待 5000ms 后才真正停止上游 flow
onStop() {// 此時下游停止收集,但上游繼續運行 5000ms
}
- 顯式取消協程:
val job = lifecycleScope.launch {viewModel.temperature.collect { }
}// 取消協程會停止收集
job.cancel() // 上游會在 5000ms 后停止
- Fragment/Activity 銷毀:
onDestroy() {// lifecycleScope 取消導致收集停止// 上游會在 5000ms 后停止
}
WhileSubscribed(5000)
的好處是:
- 短時間內重新訂閱時(如快速切換頁面)無需重啟上游 flow
- 避免頻繁啟停上游帶來的開銷
- 5000ms 后才真正停止,可以平衡資源使用和響應性
所以它特別適合:
- 需要共享的開銷較大的數據流
- 頁面快速切換的場景
- 需要緩存最新值的場景