本文為 Jose Alcérreca 發布于 Medium 的文章譯文
原文鏈接為 Migrating from LiveData to Kotlin’s Flow
本文僅作為個人學習記錄所用。如有涉及侵權,請相關人士盡快聯系譯文作者。
LiveData 是在 2017 年被大家所開始使用,觀察者模式有效簡化了開發,但 RxJava 等選項在當時對于初學者來說太復雜了。 Android 架構組件團隊創建了 LiveData:一個非常固執的可觀察數據持有者類,專為 Android 設計。 它保持簡單以使其易于上手,并且建議將 RxJava 用于更復雜的反應式流案例,利用兩者之間的集成。
DeadData?
LiveData 仍然是我們為 Java 開發人員、初學者和簡單情況提供的解決方案。 對于其余的,一個不錯的選擇是轉向 Kotlin Flows。 Flows 仍然有一個陡峭的學習曲線,但它們是 Kotlin 語言的一部分,由 Jetbrains 提供支持; Compose 即將到來,它非常適合反應式模型。
我們一直在談論使用 Flows 來連接應用程序的不同部分,除了視圖和 ViewModel。 現在我們有了一種從 Android UI 收集流的更安全的方法,我們可以創建一個完整的遷移指南。
在這篇文章中,您將學習如何將 Flows 暴露給一個視圖,如何收集它們,以及如何對其進行微調以滿足特定需求。我們一直在談論使用 Flows 來連接應用程序的不同部分,除了視圖和 ViewModel。 現在我們有了一種從 Android UI 收集流的更安全的方法,我們可以創建一個完整的遷移指南。
在這篇文章中,您將學習如何將 Flows 暴露給一個視圖,如何收集它們,以及如何對其進行微調以滿足特定需求。我們一直在談論使用 Flows 來連接應用程序的不同部分,除了視圖和 ViewModel。 現在我們有了一種從 Android UI 收集流的更安全的方法,我們可以創建一個完整的遷移指南。
在這篇文章中,您將學習如何將 Flows 暴露給一個視圖,如何收集它們,以及如何對其進行微調以滿足特定需求。
Flow:簡單的事情更難,復雜的事情更容易
LiveData 做了一件事并且做得很好:它在緩存最新值和了解 Android 的生命周期的同時公開數據。 后來我們了解到它也可以啟動協程并創建復雜的轉換,但這有點復雜。
讓我們看看一些 LiveData 模式和它們的 Flow 等價物:
#1:使用可變數據持有者公開一次性操作的結果
這是經典模式,您可以使用協程的結果來改變狀態持有者:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class MyViewModel {
private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
val myUiState: LiveData<Result<UiState>> = _myUiState
// Load data from a suspend fun and mutate state
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
為了對 Flows 做同樣的事情,我們使用 (Mutable)StateFlow:
class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState
// Load data from a suspend fun and mutate state
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
StateFlow 是一種特殊的 SharedFlow(它是一種特殊類型的 Flow),最接近 LiveData:
它總是有價值的。
它只有一個值。
它支持多個觀察者(因此流程是共享的)。
它總是 replays 訂閱的最新值,與活躍觀察者的數量無關。
向視圖公開 UI 狀態時,請使用 StateFlow。 它是一個安全高效的觀察者,旨在保持 UI 狀態。
#2:公開一次性操作的結果
這與前面的代碼片段等效,公開了沒有可變支持屬性的協程調用的結果。
對于 LiveData,我們為此使用了 liveData 協程構建器:
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于狀態持有者總是有一個值,因此最好將我們的 UI 狀態包裝在某種支持 Loading、Success 和 Error 等狀態的 Result 類中。
Flow 等效項涉及更多,因為您必須進行一些配置:
class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), // Or Lazily because it's a one-shot
initialValue = Result.Loading
)
}
stateIn 是將 Flow 轉換為 StateFlow 的 Flow 運算符。 現在讓我們相信這些參數,因為我們稍后需要更多的復雜性來正確解釋它。
3:帶參數的一次性數據加載
假設您想加載一些取決于用戶 ID 的數據,并且您從暴露流的 AuthManager 獲取此信息:
使用 LiveData,您將執行類似以下操作:
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
liveData { emit(repository.fetchItem(newUserId)) }
}
}
switchMap 是一個轉換,它的主體被執行,并且當 userId 改變時,訂閱的結果也隨之改變。
如果 userId 沒有理由成為 LiveData,那么更好的替代方法是將流與 Flow 結合起來,最后將公開的結果轉換為 LiveData。
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }
val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.asLiveData()
}
使用 Flows 執行此操作看起來非常相似:
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }
val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
請注意,如果你需要更大的靈活性,您還可以使用 transformLatest 并顯式 emit 項目:
val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser // Note the different Loading states
)
4:觀察帶參數的數據流
現在讓我們讓這個例子更具反應性。 數據不是獲取的,而是觀察到的,因此我們將數據源中的更改自動傳播到 UI。
繼續我們的例子:我們沒有在數據源上調用 fetchItem,而是使用一個假設的 observeItem 操作符,它返回一個 Flow。
使用 LiveData,您可以將 Flow 轉換為 LiveData 并 emitSource 所有更新:
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result = userId.switchMap { newUserId ->
repository.observeItem(newUserId).asLiveData()
}
}
或者,最好使用 flatMapLatest 組合兩個流,并僅將輸出轉換為 LiveData:
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }
val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.asLiveData()
}
Flow 的實現類似,但沒有 LiveData 轉換:
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }
val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser
)
}
每當用戶更改或存儲庫中的用戶數據更改時,公開的 StateFlow 都會收到更新。
#5 組合多個來源:MediatorLiveData -> Flow.combine
MediatorLiveData 可讓您觀察一個或多個更新源(LiveData 可觀察對象)并在它們獲得新數據時執行某些操作。 通常你更新 MediatorLiveData 的值:
val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...
val result = MediatorLiveData<Int>()
result.addSource(liveData1) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
Flow 等價物更直接:
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
您還可以使用 combineTransform 函數或 zip。
配置暴露的 StateFlow(stateIn 操作符)
我們之前使用 stateIn 將常規流轉換為 StateFlow,但它需要一些配置。 如果你現在不想詳細介紹,只需要復制粘貼,我推薦這種組合:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
但是,如果您不確定這個看似隨機的 5 秒 started 參數,請繼續閱讀。
stateIn 有 3 個參數(來自文檔):
@param scope the coroutine scope in which sharing is started.
@param started the strategy that controls when sharing is started and stopped.
@param initialValue the initial value of the state flow.
This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with thereplayExpirationMillis
parameter.
started可以采用 3 個值:
- Lazily:在第一個訂閱者出現時開始,在范圍取消時停止。
- Eagerly:立即開始并在范圍取消時停止
- WhileSubscribed:這很復雜。
對于一次性操作,您可以使用 Lazily 或 Eagerly。 但是,如果您正在觀察其他流程,則應該使用 WhileSubscribed 來執行小而重要的優化,如下所述。
WhileSubscribed 策略
WhileSubscribed 在沒有收集器時取消 upstream flow。 使用 stateIn 創建的 StateFlow 向 View 公開數據,但它也在觀察來自其他層或應用程序(上游)的流。 保持這些流處于活動狀態可能會導致資源浪費,例如,如果它們繼續從其他來源(例如數據庫連接、硬件傳感器等)讀取數據。**When your app goes to the background, you should be a good citizen and stop these coroutines.
WhileSubscribed 有兩個參數:
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
停止超時
來至于它的文檔:
stopTimeoutMillis 配置最后一個訂閱者消失和上游流停止之間的延遲(以毫秒為單位)。 它默認為零(立即停止)。
這很有用,因為如果視圖停止偵聽幾分之一秒,您不想取消上游流。 這一直發生。例如,當用戶旋轉設備并且視圖被快速連續地破壞和重新創建時。
liveData 協程構建器中的解決方案是添加 5 秒的延遲,如果沒有訂閱者,協程將在此后停止。 WhileSubscribed(5000) 正是這樣做的:
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
這種方法檢查所有框:
- 當用戶將您的應用程序發送到后臺時,來自其他層的更新將在 5 秒后停止,從而節省電量。
- 最新的值仍會被緩存,這樣當用戶回到它時,視圖會立即有一些數據。
- 訂閱重新啟動,新值將出現,可用時刷新屏幕。
Replay expiration
如果您不希望用戶在他們離開太久后看到陳舊數據并且你更喜歡顯示加載屏幕,請查看 WhileSubscribed 中的 replayExpirationMillis 參數。 在這種情況下它非常方便,并且還節省了一些內存,因為緩存的值恢復到 stateIn 中定義的初始值。 返回應用程序不會那么快,但您不會顯示舊數據。
replayExpirationMillis— configures a delay (in milliseconds) between the stopping of the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the shareIn operator and resets the cached value to the original initialValue for the stateIn operator). It defaults to Long.MAX_VALUE (keep replay cache forever, never reset buffer). Use zero value to expire the cache immediately.
從視圖中觀察 StateFlow
到目前為止,我們已經看到,讓視圖讓 ViewModel 中的 StateFlows 知道它們不再監聽是非常重要的。 然而,與生命周期相關的所有事情一樣,事情并沒有那么簡單。
為了收集流,你需要一個協程。 活動和片段提供了一堆協程構建器:
- Activity.lifecycleScope.launch:立即啟動協程,活動銷毀時取消。
- Fragment.lifecycleScope.launch:立即啟動協程,并在片段銷毀時取消協程。
- Fragment.viewLifecycleOwner.lifecycleScope.launch:立即啟動協程,并在片段的視圖生命周期被銷毀時取消協程。 如果您正在修改 UI,您應該使用視圖生命周期。
LaunchWhenStarted、launchWhenResumed…
稱為 launchWhenX 的特殊版本的 launch 將等到 lifecycleOwner 處于X 狀態并在lifecycleOwner 低于X 狀態時暫停協程。 重要的是要注意,在其生命周期所有者被銷毀之前,它們不會取消協程。
在應用程序處于后臺時接收更新可能會導致崩潰,這可以通過暫停視圖中的集合來解決。 但是,當應用程序在后臺時,上游流會保持活動狀態,這可能會浪費資源。
這意味著到目前為止我們為配置 StateFlow 所做的一切都將毫無用處; 然而,這是一個新的 API。
Lifecycle.repeatOnLifecycle 來救援
這個新的協程構建器(可從生命周期運行時-ktx 2.4.0-alpha01 獲得)正是我們所需要的:它在特定狀態下啟動協程,并在生命周期所有者低于它時停止它們。
例如,在一個 Fragment 中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
這將在 Fragment 的視圖 STARTED 開始收集,將繼續通過 RESUMED,并在返回到 STOPPED 時停止。可以讀下這篇文章: A safer way to collect flows from Android UIs
將 repeatOnLifecycle API 與上面的 StateFlow 指南混合在一起,可以在充分利用設備資源的同時獲得最佳性能。
Warning: The StateFlow support recently added to Data Binding uses
launchWhenCreated
to collect updates, and it will start using `repeatOnLifecycle``instead when it reaches stable.
For Data Binding, you should use Flows everywhere and simply add
asLiveData()
to expose them to the view. Data Binding will be updated whenlifecycle-runtime-ktx 2.4.0
goes stable.
總結:
從 ViewModel 公開數據并從視圖收集數據的最佳方法是:
?? 使用 WhileSubscribed
策略公開 StateFlow
,并帶有超時。
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
?? 使用 repeatOnLifecycle
收集。
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
任何其他組合都會使上游 Flows 保持活動狀態,從而浪費資源:
? 使用 WhileSubscribed
公開并在生命周期范圍內收集。launch
/launchWhenX
? 使用 Lazily
/Eagerly
公開并使用 repeatOnLifecycle
收集
當然,如果你不需要 Flow 的全部功能……只需使用 LiveData。 :)
以下附帶 Android 開發者官我那個對 Kolin 的 Flow 的介紹:
https://developer.android.com/kotlin/flow
在協程中,Flow 是一種可以順序發出多個值的類型,而不是只返回一個值的掛起函數。例如,您可以使用流從數據庫接收實時更新。
Flows 建立在協程之上,可以提供多個值。Flow 在概念上是可以異步計算的數據流。發出的值必須是相同的類型。例如, Flow<Int> 是一個發出整數值的流。
流與生成值序列的迭代器非常相似,但它使用掛起函數異步生成和消費值。這意味著,例如,Flow 可以安全地發出網絡請求以生成下一個值,而不會阻塞主線程。
數據流涉及三個實體:
生產者產生添加到流中的數據。多虧了協程,流也可以異步產生數據。
(可選)中介可以修改發送到流中的每個值或流本身。
消費者使用流中的值。
在 Android 中,存儲庫通常是 UI 數據的生產者,其用戶界面 (UI) 作為最終顯示數據的使用者。 其他時候,UI 層是用戶輸入事件的生產者,而層次結構的其他層則使用它們。 生產者和消費者之間的層通常充當中間人,修改數據流以使其適應下一層的要求。
創建一個 Flow
要創建 flows,請使用 flow builder APIs。 Flow 構建器函數創建一個新 Flow,您可以在其中使用發射函數手動將新值 emit 到數據流中。
在以下示例中,數據源以固定時間間隔自動獲取最新消息。 由于掛起函數不能返回多個連續值,因此數據源創建并返回一個 Flow 來滿足此要求。 在這種情況下,數據源充當生產者。
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val refreshIntervalMs: Long = 5000
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews) // Emits the result of the request to the flow
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}
// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
flow builder 在協程中執行。 因此,它受益于相同的異步 API,但存在一些限制:
Flows 是連續的。 由于生產者在協程中,當調用掛起函數時,生產者掛起直到掛起函數返回。 在這個例子中,生產者掛起直到 fetchLatestNews 網絡請求完成。 只有這樣,結果才會發送到流中。
使用流構建器,生產者不能從不同的 CoroutineContext 發出值。 因此,不要通過創建新的協程或使用 withContext 代碼塊在不同的 CoroutineContext 中調用發射。 在這些情況下,您可以使用其他流構建器,例如 callbackFlow。
修改流
中介可以使用中間操作符來修改數據流而不消耗值。 這些運算符是函數,當應用于數據流時,會設置一系列操作,直到將來使用這些值時才會執行這些操作。 在 Flow reference documentation 中了解有關中間運算符的更多信息。
在下面的示例中,存儲庫層使用中間運算符 map 來轉換要在視圖上顯示的數據:
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val userData: UserData
) {
/**
* Returns the favorite latest news applying transformations on the flow.
* These operations are lazy and don't trigger the flow. They just transform
* the current value emitted by the flow at that point in time.
*/
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
// Intermediate operation to filter the list of favorite topics
.map { news -> news.filter { userData.isFavoriteTopic(it) } }
// Intermediate operation to save the latest news in the cache
.onEach { news -> saveInCache(news) }
}
中間運算符可以一個接一個地應用,形成一個操作鏈,當一個項目被發送到 Flow 中時,這些操作鏈會延遲執行。 請注意,簡單地將中間運算符應用于流并不會啟動 Flow 集合。
從 Flow 中收集
使用終端運算符觸發 Flow 以開始偵聽值。 要獲取流中發出的所有值,請使用 collect。
由于 collect 是一個掛起函數,它需要在協程中執行。 它接受一個 lambda 作為參數,在每個新值上調用該參數。 由于它是一個掛起函數,調用 collect 的協程可能會掛起,直到 Flow 關閉。
繼續前面的示例,這里是一個使用存儲庫層數據的 ViewModel 的簡單實現:
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
// Trigger the flow and consume its elements using collect
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// Update View with the latest favorite news
}
}
}
}
收集 Flow 觸發更新最新消息的生產者,并以固定的時間間隔發出網絡請求的結果。由于生產者在 while(true)
循環中始終保持活動狀態,因此當 ViewModel 被清除并取消 viewModelScope
時,數據流將關閉。
由于以下原因,Flow 收集可能會停止:
收集的協程被取消,如上例所示。這也阻止了底層生產者。
生產者完成發射項目。在這種情況下,數據流關閉,調用
collect
的協程恢復執行。
除非與其他中間操作符指定,否則 Flow 是冷的和惰性的。這意味著每次在流上調用終端操作符時都會執行生產者代碼。在前面的示例中,擁有多個流收集器會導致數據源以不同的固定時間間隔多次獲取最新消息。要在多個消費者同時收集時優化和共享流,請使用 shareIn 運算符。
捕獲意外異常
生產者的實現可以來自第三方庫。 這意味著它可以拋出意外的異常。 要處理這些異常,請使用 catch 中間運算符。
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Intermediate catch operator. If an exception is thrown,
// catch and update the UI
.catch { exception -> notifyError(exception) }
.collect { favoriteNews ->
// Update View with the latest favorite news
}
}
}
}
在前面的示例中,當發生異常時,不會調用 collect
lambda,因為尚未收到新項目。
catch
還可以向流 emit
項目。 示例存儲庫層可以改為 emit
緩存值:
class NewsRepository(...) {
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
.map { news -> news.filter { userData.isFavoriteTopic(it) } }
.onEach { news -> saveInCache(news) }
// If an error happens, emit the last cached values
.catch { exception -> emit(lastCachedNews()) }
}
在這個例子中,當一個異常發生時,collect
lambda 被調用,因為一個新的項目因為異常被發送到流中。
在不同的 CoroutineContext 中執行
默認情況下,Flow
構建器的生產者在從它收集的協程的 CoroutineContext
中執行,并且如前所述,它不能從不同的 CoroutineContext
發出值。 在某些情況下,這種行為可能是不可取的。 例如,在本主題中使用的示例中,存儲庫層不應在 viewModelScope
使用的 Dispatchers.Main
上執行操作。
要更改流的 CoroutineContext,請使用中間運算符 flowOn。 flowOn 改變了上游流的 CoroutineContext,這意味著生產者和任何在 flowOn 之前(或之上)應用的中間操作符。 下游流(flowOn 之后的中間運算符以及消費者)不受影響,并在用于從流中收集的 CoroutineContext 上執行。 如果有多個 flowOn 操作符,每個操作符都會改變其當前位置的上游。
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val userData: UserData,
private val defaultDispatcher: CoroutineDispatcher
) {
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
.map { news -> // Executes on the default dispatcher
news.filter { userData.isFavoriteTopic(it) }
}
.onEach { news -> // Executes on the default dispatcher
saveInCache(news)
}
// flowOn affects the upstream flow ↑
.flowOn(defaultDispatcher)
// the downstream flow ↓ is not affected
.catch { exception -> // Executes in the consumer's context
emit(lastCachedNews())
}
}
使用此代碼,onEach
和 map
操作符使用 defaultDispatcher
,而 catch
操作符和使用者在 viewModelScope
使用的 Dispatchers.Main
上執行。
由于數據源層正在進行 I/O 工作,因此您應該使用針對 I/O 操作進行優化的調度程序:
class NewsRemoteDataSource(
...,
private val ioDispatcher: CoroutineDispatcher
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
// Executes on the IO dispatcher
...
}
.flowOn(ioDispatcher)
}
Jetpack 庫中的流程
Flow 被集成到許多 Jetpack 庫中,它在 Android 第三方庫中很受歡迎。 Flow 非常適合實時數據更新和無休止的數據流。
您可以使用 Flow with Room 來通知數據庫中的更改。 使用數據訪問對象 data access objects (DAO) 時,返回 Flow
類型以獲取實時更新。
@Dao
abstract class ExampleDao {
@Query("SELECT * FROM Example")
abstract fun getExamples(): Flow<List<Example>>
}
每次示例表中發生更改時,都會發出一個包含數據庫中新項目的新列表。
將基于回調的 API 轉換為流
callbackFlow 是一個流構建器,可讓您將基于回調的 API 轉換為流。 例如, Firebase Firestore Android API 使用回調。 要將這些 API 轉換為流并偵聽 Firestore 數據庫更新,您可以使用以下代碼:
class FirestoreUserEventsDataSource(
private val firestore: FirebaseFirestore
) {
// Method to get user events from the Firestore database
fun getUserEvents(): Flow<UserEvents> = callbackFlow {
// Reference to use in Firestore
var eventsCollection: CollectionReference? = null
try {
eventsCollection = FirebaseFirestore.getInstance()
.collection("collection")
.document("app")
} catch (e: Throwable) {
// If Firebase cannot be initialized, close the stream of data
// flow consumers will stop collecting and the coroutine will resume
close(e)
}
// Registers callback to firestore, which will be called on new events
val subscription = eventsCollection?.addSnapshotListener { snapshot, _ ->
if (snapshot == null) { return@addSnapshotListener }
// Sends events to the flow! Consumers will get the new events
try {
offer(snapshot.getEvents())
} catch (e: Throwable) {
// Event couldn't be sent to the flow
}
}
// The callback inside awaitClose will be executed when the flow is
// either closed or cancelled.
// In this case, remove the callback from Firestore
awaitClose { subscription?.remove() }
}
}
與 Flow
構建器不同,callbackFlow
允許使用 send 函數從不同的 CoroutineContext
發出值,或者使用 offer 函數從協程外部發出值。
在內部,callbackFlow
使用一個 channel,它在概念上與阻塞 queue 非常相似。 一個通道配置了一個容量,即可以緩沖的最大元素數。 在 callbackFlow
中創建的通道默認容量為 64 個元素。 當您嘗試將新元素添加到完整頻道時,發送會暫停生產者,直到有新元素的空間,而 offer
不會將元素添加到頻道并立即返回 false
。
額外 Flow 資料鏈接: