Flow.shareIn 與 Flow.stateIn 操作符可以將冷流轉換為熱流: 它們可以將來自上游冷數據流的信息廣播給多個收集者。這兩個操作符通常用于提升性能: 在沒有收集者時加入緩沖;或者干脆作為一種緩存機制使用。
注意: 冷流 是按需創建的,并且會在它們被觀察時發送數據;熱流 則總是活躍,無論是否被觀察,它們都能發送數據。
本文將會通過示例幫您熟悉 shareIn 與 stateIn 操作符。您將學到如何針對特定用例配置它們,并避免可能遇到的常見陷阱。
底層數據流生產者
繼續使用我 之前文章 中使用過的例子——使用底層數據流生產者發出位置更新。它是一個使用 callbackFlow 實現的 冷流。每個新的收集者都會觸發數據流的生產者代碼塊,同時也會將新的回調加入到 FusedLocationProviderClient。
class LocationDataSource(
private val locationClient: FusedLocationProviderClient
) {
val locationsSource: Flow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// 在 Flow 結束收集時進行清理
awaitClose {
removeLocationUpdates(callback)
}
}
}
讓我們看看在不同的用例下如何使用 shareIn 與 stateIn 優化 locationsSource 數據流。
shareIn 還是 stateIn?
我們要討論的第一個話題是 shareIn
與 stateIn
之間的區別。shareIn
操作符返回的是 SharedFlow 而 stateIn
返回的是 StateFlow。
注意 : 要了解有關
StateFlow
與SharedFlow
的更多信息,可以查看 我們的文檔 。
StateFlow 是 SharedFlow 的一種特殊配置,旨在優化分享狀態: 最后被發送的項目會重新發送給新的收集者,并且這些項目會使用 Any.equals 進行合并。您可以在 StateFlow 文檔 中查看更多相關信息。
兩者之間的最主要區別,在于 StateFlow
接口允許您通過讀取 value
屬性同步訪問其最后發出的值。而這不是 SharedFlow
的使用方式。
提升性能
通過共享所有收集者要觀察的同一數據流實例 (而不是按需創建同一個數據流的新實例),這些 API 可以為我們提升性能。
在下面的例子中,LocationRepository
消費了 LocationDataSource
暴露的 locationsSource
數據流,同時使用了 shareIn 操作符,從而讓每個對用戶位置信息感興趣的收集者都從同一數據流實例中收集數據。這里只創建了一個 locationsSource
數據流實例并由所有收集者共享:
class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}
WhileSubscribed 共享策略用于在沒有收集者時取消上游數據流。這樣一來,我們便能在沒有程序對位置更新感興趣時避免資源的浪費。
Android 應用小提醒! 在大部分情況下,您可以使用 WhileSubscribed(5000),當最后一個收集者消失后再保持上游數據流活躍狀態 5 秒鐘。這樣在某些特定情況 (如配置改變) 下可以避免重啟上游數據流。當上游數據流的創建成本很高,或者在 ViewModel 中使用這些操作符時,這一技巧尤其有用。
緩沖事件
在下面的例子中,我們的需求有所改變。現在要求我們保持監聽位置更新,同時要在應用從后臺返回前臺時在屏幕上顯示最后的 10 個位置:
class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource
.shareIn(externalScope, SharingStarted.Eagerly, replay = 10)
}
我們將參數 replay
的值設置為 10,來讓最后發出的 10 個項目保持在內存中,同時在每次有收集者觀察數據流時重新發送這些項目。為了保持內部數據流始終處于活躍狀態并發送位置更新,我們使用了共享策略 SharingStarted.Eagerly
,這樣就算沒有收集者,也能一直監聽更新。
緩存數據
我們的需求再次發生變化,這次我們不再需要應用處于后臺時 持續 監聽位置更新。不過,我們需要緩存最后發送的項目,讓用戶在獲取當前位置時能在屏幕上看到一些數據 (即使數據是舊的)。針對這種情況,我們可以使用 stateIn 操作符。
class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}
Flow.stateIn
可以緩存最后發送的項目,并重放給新的收集者。
注意!不要在每個函數調用時創建新的實例
切勿 在調用某個函數調用返回時,使用 shareIn 或 stateIn 創建新的數據流。這樣會在每次函數調用時創建一個新的 SharedFlow 或 StateFlow,而它們將會一直保持在內存中,直到作用域被取消或者在沒有任何引用時被垃圾回收。
class UserRepository(
private val userLocalDataSource: UserLocalDataSource,
private val externalScope: CoroutineScope
) {
// 不要像這樣在函數中使用 shareIn 或 stateIn
// 這將在每次調用時創建新的 SharedFlow 或 StateFlow,而它們將不會被復用。
fun getUser(): Flow<User> =
userLocalDataSource.getUser()
.shareIn(externalScope, WhileSubscribed())
// 可以在屬性中使用 shareIn 或 stateIn
val user: Flow<User> =
userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}
需要入參的數據流
需要入參 (如 userId
) 的數據流無法簡單地使用 shareIn
或 stateIn
共享。以開源項目——Google I/O 的 Android 應用 iosched 為例,您可以在 源碼中 看到,從 Firestore 獲取用戶事件的數據流是通過 callbackFlow
實現的。由于其接收 userId
作為參數,因此無法簡單使用 shareIn
或 stateIn
操作符對其進行復用。
class UserRepository(
private val userEventsDataSource: FirestoreUserEventDataSource
) {
// 新的收集者會在 Firestore 中注冊為新的回調。
// 由于這一函數依賴一個 `userId`,所以在這個函數中
// 數據流無法通過調用 shareIn 或 stateIn 進行復用.
// 這樣會導致每次調用函數時,都會創建新的 SharedFlow 或 StateFlow
fun getUserEvents(userId: String): Flow<UserEventsResult> =
userLocalDataSource.getObservableUserEvents(userId)
}
如何優化這一用例取決于您應用的需求:
- 您是否允許同時從多個用戶接收事件?如果答案是肯定的,您可能需要為
SharedFlow
或StateFlow
實例創建一個 map,并在subscriptionCount
為 0 時移除引用并退出上游數據流。 - 如果您只允許一個用戶,并且收集者需要更新為觀察新的用戶,您可以向一個所有收集者共用的
SharedFlow
或StateFlow
發送事件更新,并將公共數據流作為類中的變量。
shareIn
與 stateIn
操作符可以與冷流一同使用來提升性能,您可以使用它們在沒有收集者時添加緩沖,或者直接將其作為緩存機制使用。小心使用它們,不要在每次函數調用時都創建新的數據流實例——這樣會導致資源的浪費及預料之外的問題!