本文為 Jose Alcérreca 發(fā)布于 Medium 的文章譯文
原文鏈接為 Migrating from LiveData to Kotlin’s Flow
本文僅作為個(gè)人學(xué)習(xí)記錄所用。如有涉及侵權(quán),請(qǐng)相關(guān)人士盡快聯(lián)系譯文作者。
LiveData 是在 2017 年被大家所開始使用,觀察者模式有效簡化了開發(fā),但 RxJava 等選項(xiàng)在當(dāng)時(shí)對(duì)于初學(xué)者來說太復(fù)雜了。 Android 架構(gòu)組件團(tuán)隊(duì)創(chuàng)建了 LiveData:一個(gè)非常固執(zhí)的可觀察數(shù)據(jù)持有者類,專為 Android 設(shè)計(jì)。 它保持簡單以使其易于上手,并且建議將 RxJava 用于更復(fù)雜的反應(yīng)式流案例,利用兩者之間的集成。
DeadData?
LiveData 仍然是我們?yōu)?Java 開發(fā)人員、初學(xué)者和簡單情況提供的解決方案。 對(duì)于其余的,一個(gè)不錯(cuò)的選擇是轉(zhuǎn)向 Kotlin Flows。 Flows 仍然有一個(gè)陡峭的學(xué)習(xí)曲線,但它們是 Kotlin 語言的一部分,由 Jetbrains 提供支持; Compose 即將到來,它非常適合反應(yīng)式模型。
我們一直在談?wù)撌褂?Flows 來連接應(yīng)用程序的不同部分,除了視圖和 ViewModel。 現(xiàn)在我們有了一種從 Android UI 收集流的更安全的方法,我們可以創(chuàng)建一個(gè)完整的遷移指南。
在這篇文章中,您將學(xué)習(xí)如何將 Flows 暴露給一個(gè)視圖,如何收集它們,以及如何對(duì)其進(jìn)行微調(diào)以滿足特定需求。我們一直在談?wù)撌褂?Flows 來連接應(yīng)用程序的不同部分,除了視圖和 ViewModel。 現(xiàn)在我們有了一種從 Android UI 收集流的更安全的方法,我們可以創(chuàng)建一個(gè)完整的遷移指南。
在這篇文章中,您將學(xué)習(xí)如何將 Flows 暴露給一個(gè)視圖,如何收集它們,以及如何對(duì)其進(jìn)行微調(diào)以滿足特定需求。我們一直在談?wù)撌褂?Flows 來連接應(yīng)用程序的不同部分,除了視圖和 ViewModel。 現(xiàn)在我們有了一種從 Android UI 收集流的更安全的方法,我們可以創(chuàng)建一個(gè)完整的遷移指南。
在這篇文章中,您將學(xué)習(xí)如何將 Flows 暴露給一個(gè)視圖,如何收集它們,以及如何對(duì)其進(jìn)行微調(diào)以滿足特定需求。
Flow:簡單的事情更難,復(fù)雜的事情更容易
LiveData 做了一件事并且做得很好:它在緩存最新值和了解 Android 的生命周期的同時(shí)公開數(shù)據(jù)。 后來我們了解到它也可以啟動(dòng)協(xié)程并創(chuàng)建復(fù)雜的轉(zhuǎn)換,但這有點(diǎn)復(fù)雜。
讓我們看看一些 LiveData 模式和它們的 Flow 等價(jià)物:
#1:使用可變數(shù)據(jù)持有者公開一次性操作的結(jié)果
這是經(jīng)典模式,您可以使用協(xié)程的結(jié)果來改變狀態(tài)持有者:
<!-- 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
}
}
}
為了對(duì) 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:
它總是有價(jià)值的。
它只有一個(gè)值。
它支持多個(gè)觀察者(因此流程是共享的)。
它總是 replays 訂閱的最新值,與活躍觀察者的數(shù)量無關(guān)。
向視圖公開 UI 狀態(tài)時(shí),請(qǐng)使用 StateFlow。 它是一個(gè)安全高效的觀察者,旨在保持 UI 狀態(tài)。
#2:公開一次性操作的結(jié)果
這與前面的代碼片段等效,公開了沒有可變支持屬性的協(xié)程調(diào)用的結(jié)果。
對(duì)于 LiveData,我們?yōu)榇耸褂昧?liveData 協(xié)程構(gòu)建器:
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于狀態(tài)持有者總是有一個(gè)值,因此最好將我們的 UI 狀態(tài)包裝在某種支持 Loading、Success 和 Error 等狀態(tài)的 Result 類中。
Flow 等效項(xiàng)涉及更多,因?yàn)槟仨氝M(jìn)行一些配置:
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 轉(zhuǎn)換為 StateFlow 的 Flow 運(yùn)算符。 現(xiàn)在讓我們相信這些參數(shù),因?yàn)槲覀兩院笮枰嗟膹?fù)雜性來正確解釋它。
3:帶參數(shù)的一次性數(shù)據(jù)加載
假設(shè)您想加載一些取決于用戶 ID 的數(shù)據(jù),并且您從暴露流的 AuthManager 獲取此信息:
使用 LiveData,您將執(zhí)行類似以下操作:
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 是一個(gè)轉(zhuǎn)換,它的主體被執(zhí)行,并且當(dāng) userId 改變時(shí),訂閱的結(jié)果也隨之改變。
如果 userId 沒有理由成為 LiveData,那么更好的替代方法是將流與 Flow 結(jié)合起來,最后將公開的結(jié)果轉(zhuǎn)換為 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 執(zhí)行此操作看起來非常相似:
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
)
}
請(qǐng)注意,如果你需要更大的靈活性,您還可以使用 transformLatest 并顯式 emit 項(xiàng)目:
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:觀察帶參數(shù)的數(shù)據(jù)流
現(xiàn)在讓我們讓這個(gè)例子更具反應(yīng)性。 數(shù)據(jù)不是獲取的,而是觀察到的,因此我們將數(shù)據(jù)源中的更改自動(dòng)傳播到 UI。
繼續(xù)我們的例子:我們沒有在數(shù)據(jù)源上調(diào)用 fetchItem,而是使用一個(gè)假設(shè)的 observeItem 操作符,它返回一個(gè) Flow。
使用 LiveData,您可以將 Flow 轉(zhuǎn)換為 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 組合兩個(gè)流,并僅將輸出轉(zhuǎn)換為 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 的實(shí)現(xiàn)類似,但沒有 LiveData 轉(zhuǎn)換:
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
)
}
每當(dāng)用戶更改或存儲(chǔ)庫中的用戶數(shù)據(jù)更改時(shí),公開的 StateFlow 都會(huì)收到更新。
#5 組合多個(gè)來源:MediatorLiveData -> Flow.combine
MediatorLiveData 可讓您觀察一個(gè)或多個(gè)更新源(LiveData 可觀察對(duì)象)并在它們獲得新數(shù)據(jù)時(shí)執(zhí)行某些操作。 通常你更新 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 等價(jià)物更直接:
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
您還可以使用 combineTransform 函數(shù)或 zip。
配置暴露的 StateFlow(stateIn 操作符)
我們之前使用 stateIn 將常規(guī)流轉(zhuǎn)換為 StateFlow,但它需要一些配置。 如果你現(xiàn)在不想詳細(xì)介紹,只需要復(fù)制粘貼,我推薦這種組合:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
但是,如果您不確定這個(gè)看似隨機(jī)的 5 秒 started 參數(shù),請(qǐng)繼續(xù)閱讀。
stateIn 有 3 個(gè)參數(shù)(來自文檔):
@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 個(gè)值:
- Lazily:在第一個(gè)訂閱者出現(xiàn)時(shí)開始,在范圍取消時(shí)停止。
- Eagerly:立即開始并在范圍取消時(shí)停止
- WhileSubscribed:這很復(fù)雜。
對(duì)于一次性操作,您可以使用 Lazily 或 Eagerly。 但是,如果您正在觀察其他流程,則應(yīng)該使用 WhileSubscribed 來執(zhí)行小而重要的優(yōu)化,如下所述。
WhileSubscribed 策略
WhileSubscribed 在沒有收集器時(shí)取消 upstream flow。 使用 stateIn 創(chuàng)建的 StateFlow 向 View 公開數(shù)據(jù),但它也在觀察來自其他層或應(yīng)用程序(上游)的流。 保持這些流處于活動(dòng)狀態(tài)可能會(huì)導(dǎo)致資源浪費(fèi),例如,如果它們繼續(xù)從其他來源(例如數(shù)據(jù)庫連接、硬件傳感器等)讀取數(shù)據(jù)。**When your app goes to the background, you should be a good citizen and stop these coroutines.
WhileSubscribed 有兩個(gè)參數(shù):
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
停止超時(shí)
來至于它的文檔:
stopTimeoutMillis 配置最后一個(gè)訂閱者消失和上游流停止之間的延遲(以毫秒為單位)。 它默認(rèn)為零(立即停止)。
這很有用,因?yàn)槿绻晥D停止偵聽?zhēng)追种幻耄幌肴∠嫌瘟鳌?這一直發(fā)生。例如,當(dāng)用戶旋轉(zhuǎn)設(shè)備并且視圖被快速連續(xù)地破壞和重新創(chuàng)建時(shí)。
liveData 協(xié)程構(gòu)建器中的解決方案是添加 5 秒的延遲,如果沒有訂閱者,協(xié)程將在此后停止。 WhileSubscribed(5000) 正是這樣做的:
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
這種方法檢查所有框:
- 當(dāng)用戶將您的應(yīng)用程序發(fā)送到后臺(tái)時(shí),來自其他層的更新將在 5 秒后停止,從而節(jié)省電量。
- 最新的值仍會(huì)被緩存,這樣當(dāng)用戶回到它時(shí),視圖會(huì)立即有一些數(shù)據(jù)。
- 訂閱重新啟動(dòng),新值將出現(xiàn),可用時(shí)刷新屏幕。
Replay expiration
如果您不希望用戶在他們離開太久后看到陳舊數(shù)據(jù)并且你更喜歡顯示加載屏幕,請(qǐng)查看 WhileSubscribed 中的 replayExpirationMillis 參數(shù)。 在這種情況下它非常方便,并且還節(jié)省了一些內(nèi)存,因?yàn)榫彺娴闹祷謴?fù)到 stateIn 中定義的初始值。 返回應(yīng)用程序不會(huì)那么快,但您不會(huì)顯示舊數(shù)據(jù)。
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
到目前為止,我們已經(jīng)看到,讓視圖讓 ViewModel 中的 StateFlows 知道它們不再監(jiān)聽是非常重要的。 然而,與生命周期相關(guān)的所有事情一樣,事情并沒有那么簡單。
為了收集流,你需要一個(gè)協(xié)程。 活動(dòng)和片段提供了一堆協(xié)程構(gòu)建器:
- Activity.lifecycleScope.launch:立即啟動(dòng)協(xié)程,活動(dòng)銷毀時(shí)取消。
- Fragment.lifecycleScope.launch:立即啟動(dòng)協(xié)程,并在片段銷毀時(shí)取消協(xié)程。
- Fragment.viewLifecycleOwner.lifecycleScope.launch:立即啟動(dòng)協(xié)程,并在片段的視圖生命周期被銷毀時(shí)取消協(xié)程。 如果您正在修改 UI,您應(yīng)該使用視圖生命周期。
LaunchWhenStarted、launchWhenResumed…
稱為 launchWhenX 的特殊版本的 launch 將等到 lifecycleOwner 處于X 狀態(tài)并在lifecycleOwner 低于X 狀態(tài)時(shí)暫停協(xié)程。 重要的是要注意,在其生命周期所有者被銷毀之前,它們不會(huì)取消協(xié)程。
在應(yīng)用程序處于后臺(tái)時(shí)接收更新可能會(huì)導(dǎo)致崩潰,這可以通過暫停視圖中的集合來解決。 但是,當(dāng)應(yīng)用程序在后臺(tái)時(shí),上游流會(huì)保持活動(dòng)狀態(tài),這可能會(huì)浪費(fèi)資源。
這意味著到目前為止我們?yōu)榕渲?StateFlow 所做的一切都將毫無用處; 然而,這是一個(gè)新的 API。
Lifecycle.repeatOnLifecycle 來救援
這個(gè)新的協(xié)程構(gòu)建器(可從生命周期運(yùn)行時(shí)-ktx 2.4.0-alpha01 獲得)正是我們所需要的:它在特定狀態(tài)下啟動(dòng)協(xié)程,并在生命周期所有者低于它時(shí)停止它們。
例如,在一個(gè) Fragment 中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
這將在 Fragment 的視圖 STARTED 開始收集,將繼續(xù)通過 RESUMED,并在返回到 STOPPED 時(shí)停止。可以讀下這篇文章: A safer way to collect flows from Android UIs
將 repeatOnLifecycle API 與上面的 StateFlow 指南混合在一起,可以在充分利用設(shè)備資源的同時(shí)獲得最佳性能。
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.
總結(jié):
從 ViewModel 公開數(shù)據(jù)并從視圖收集數(shù)據(jù)的最佳方法是:
?? 使用 WhileSubscribed
策略公開 StateFlow
,并帶有超時(shí)。
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 { ... }
}
}
}
任何其他組合都會(huì)使上游 Flows 保持活動(dòng)狀態(tài),從而浪費(fèi)資源:
? 使用 WhileSubscribed
公開并在生命周期范圍內(nèi)收集。launch
/launchWhenX
? 使用 Lazily
/Eagerly
公開并使用 repeatOnLifecycle
收集
當(dāng)然,如果你不需要 Flow 的全部功能……只需使用 LiveData。 :)
以下附帶 Android 開發(fā)者官我那個(gè)對(duì) Kolin 的 Flow 的介紹:
https://developer.android.com/kotlin/flow
在協(xié)程中,F(xiàn)low 是一種可以順序發(fā)出多個(gè)值的類型,而不是只返回一個(gè)值的掛起函數(shù)。例如,您可以使用流從數(shù)據(jù)庫接收實(shí)時(shí)更新。
Flows 建立在協(xié)程之上,可以提供多個(gè)值。Flow 在概念上是可以異步計(jì)算的數(shù)據(jù)流。發(fā)出的值必須是相同的類型。例如, Flow<Int> 是一個(gè)發(fā)出整數(shù)值的流。
流與生成值序列的迭代器非常相似,但它使用掛起函數(shù)異步生成和消費(fèi)值。這意味著,例如,F(xiàn)low 可以安全地發(fā)出網(wǎng)絡(luò)請(qǐng)求以生成下一個(gè)值,而不會(huì)阻塞主線程。
數(shù)據(jù)流涉及三個(gè)實(shí)體:
生產(chǎn)者產(chǎn)生添加到流中的數(shù)據(jù)。多虧了協(xié)程,流也可以異步產(chǎn)生數(shù)據(jù)。
(可選)中介可以修改發(fā)送到流中的每個(gè)值或流本身。
消費(fèi)者使用流中的值。
在 Android 中,存儲(chǔ)庫通常是 UI 數(shù)據(jù)的生產(chǎn)者,其用戶界面 (UI) 作為最終顯示數(shù)據(jù)的使用者。 其他時(shí)候,UI 層是用戶輸入事件的生產(chǎn)者,而層次結(jié)構(gòu)的其他層則使用它們。 生產(chǎn)者和消費(fèi)者之間的層通常充當(dāng)中間人,修改數(shù)據(jù)流以使其適應(yīng)下一層的要求。
創(chuàng)建一個(gè) Flow
要?jiǎng)?chuàng)建 flows,請(qǐng)使用 flow builder APIs。 Flow 構(gòu)建器函數(shù)創(chuàng)建一個(gè)新 Flow,您可以在其中使用發(fā)射函數(shù)手動(dòng)將新值 emit 到數(shù)據(jù)流中。
在以下示例中,數(shù)據(jù)源以固定時(shí)間間隔自動(dòng)獲取最新消息。 由于掛起函數(shù)不能返回多個(gè)連續(xù)值,因此數(shù)據(jù)源創(chuàng)建并返回一個(gè) Flow 來滿足此要求。 在這種情況下,數(shù)據(jù)源充當(dāng)生產(chǎn)者。
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 在協(xié)程中執(zhí)行。 因此,它受益于相同的異步 API,但存在一些限制:
Flows 是連續(xù)的。 由于生產(chǎn)者在協(xié)程中,當(dāng)調(diào)用掛起函數(shù)時(shí),生產(chǎn)者掛起直到掛起函數(shù)返回。 在這個(gè)例子中,生產(chǎn)者掛起直到 fetchLatestNews 網(wǎng)絡(luò)請(qǐng)求完成。 只有這樣,結(jié)果才會(huì)發(fā)送到流中。
使用流構(gòu)建器,生產(chǎn)者不能從不同的 CoroutineContext 發(fā)出值。 因此,不要通過創(chuàng)建新的協(xié)程或使用 withContext 代碼塊在不同的 CoroutineContext 中調(diào)用發(fā)射。 在這些情況下,您可以使用其他流構(gòu)建器,例如 callbackFlow。
修改流
中介可以使用中間操作符來修改數(shù)據(jù)流而不消耗值。 這些運(yùn)算符是函數(shù),當(dāng)應(yīng)用于數(shù)據(jù)流時(shí),會(huì)設(shè)置一系列操作,直到將來使用這些值時(shí)才會(huì)執(zhí)行這些操作。 在 Flow reference documentation 中了解有關(guān)中間運(yùn)算符的更多信息。
在下面的示例中,存儲(chǔ)庫層使用中間運(yùn)算符 map 來轉(zhuǎn)換要在視圖上顯示的數(shù)據(jù):
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) }
}
中間運(yùn)算符可以一個(gè)接一個(gè)地應(yīng)用,形成一個(gè)操作鏈,當(dāng)一個(gè)項(xiàng)目被發(fā)送到 Flow 中時(shí),這些操作鏈會(huì)延遲執(zhí)行。 請(qǐng)注意,簡單地將中間運(yùn)算符應(yīng)用于流并不會(huì)啟動(dòng) Flow 集合。
從 Flow 中收集
使用終端運(yùn)算符觸發(fā) Flow 以開始偵聽值。 要獲取流中發(fā)出的所有值,請(qǐng)使用 collect。
由于 collect 是一個(gè)掛起函數(shù),它需要在協(xié)程中執(zhí)行。 它接受一個(gè) lambda 作為參數(shù),在每個(gè)新值上調(diào)用該參數(shù)。 由于它是一個(gè)掛起函數(shù),調(diào)用 collect 的協(xié)程可能會(huì)掛起,直到 Flow 關(guān)閉。
繼續(xù)前面的示例,這里是一個(gè)使用存儲(chǔ)庫層數(shù)據(jù)的 ViewModel 的簡單實(shí)現(xiàn):
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 觸發(fā)更新最新消息的生產(chǎn)者,并以固定的時(shí)間間隔發(fā)出網(wǎng)絡(luò)請(qǐng)求的結(jié)果。由于生產(chǎn)者在 while(true)
循環(huán)中始終保持活動(dòng)狀態(tài),因此當(dāng) ViewModel 被清除并取消 viewModelScope
時(shí),數(shù)據(jù)流將關(guān)閉。
由于以下原因,F(xiàn)low 收集可能會(huì)停止:
收集的協(xié)程被取消,如上例所示。這也阻止了底層生產(chǎn)者。
生產(chǎn)者完成發(fā)射項(xiàng)目。在這種情況下,數(shù)據(jù)流關(guān)閉,調(diào)用
collect
的協(xié)程恢復(fù)執(zhí)行。
除非與其他中間操作符指定,否則 Flow 是冷的和惰性的。這意味著每次在流上調(diào)用終端操作符時(shí)都會(huì)執(zhí)行生產(chǎn)者代碼。在前面的示例中,擁有多個(gè)流收集器會(huì)導(dǎo)致數(shù)據(jù)源以不同的固定時(shí)間間隔多次獲取最新消息。要在多個(gè)消費(fèi)者同時(shí)收集時(shí)優(yōu)化和共享流,請(qǐng)使用 shareIn 運(yùn)算符。
捕獲意外異常
生產(chǎn)者的實(shí)現(xiàn)可以來自第三方庫。 這意味著它可以拋出意外的異常。 要處理這些異常,請(qǐng)使用 catch 中間運(yùn)算符。
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
}
}
}
}
在前面的示例中,當(dāng)發(fā)生異常時(shí),不會(huì)調(diào)用 collect
lambda,因?yàn)樯形词盏叫马?xiàng)目。
catch
還可以向流 emit
項(xiàng)目。 示例存儲(chǔ)庫層可以改為 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()) }
}
在這個(gè)例子中,當(dāng)一個(gè)異常發(fā)生時(shí),collect
lambda 被調(diào)用,因?yàn)橐粋€(gè)新的項(xiàng)目因?yàn)楫惓1话l(fā)送到流中。
在不同的 CoroutineContext 中執(zhí)行
默認(rèn)情況下,Flow
構(gòu)建器的生產(chǎn)者在從它收集的協(xié)程的 CoroutineContext
中執(zhí)行,并且如前所述,它不能從不同的 CoroutineContext
發(fā)出值。 在某些情況下,這種行為可能是不可取的。 例如,在本主題中使用的示例中,存儲(chǔ)庫層不應(yīng)在 viewModelScope
使用的 Dispatchers.Main
上執(zhí)行操作。
要更改流的 CoroutineContext,請(qǐng)使用中間運(yùn)算符 flowOn。 flowOn 改變了上游流的 CoroutineContext,這意味著生產(chǎn)者和任何在 flowOn 之前(或之上)應(yīng)用的中間操作符。 下游流(flowOn 之后的中間運(yùn)算符以及消費(fèi)者)不受影響,并在用于從流中收集的 CoroutineContext 上執(zhí)行。 如果有多個(gè) flowOn 操作符,每個(gè)操作符都會(huì)改變其當(dāng)前位置的上游。
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
上執(zhí)行。
由于數(shù)據(jù)源層正在進(jìn)行 I/O 工作,因此您應(yīng)該使用針對(duì) I/O 操作進(jìn)行優(yōu)化的調(dià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 非常適合實(shí)時(shí)數(shù)據(jù)更新和無休止的數(shù)據(jù)流。
您可以使用 Flow with Room 來通知數(shù)據(jù)庫中的更改。 使用數(shù)據(jù)訪問對(duì)象 data access objects (DAO) 時(shí),返回 Flow
類型以獲取實(shí)時(shí)更新。
@Dao
abstract class ExampleDao {
@Query("SELECT * FROM Example")
abstract fun getExamples(): Flow<List<Example>>
}
每次示例表中發(fā)生更改時(shí),都會(huì)發(fā)出一個(gè)包含數(shù)據(jù)庫中新項(xiàng)目的新列表。
將基于回調(diào)的 API 轉(zhuǎn)換為流
callbackFlow 是一個(gè)流構(gòu)建器,可讓您將基于回調(diào)的 API 轉(zhuǎn)換為流。 例如, Firebase Firestore Android API 使用回調(diào)。 要將這些 API 轉(zhuǎn)換為流并偵聽 Firestore 數(shù)據(jù)庫更新,您可以使用以下代碼:
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
構(gòu)建器不同,callbackFlow
允許使用 send 函數(shù)從不同的 CoroutineContext
發(fā)出值,或者使用 offer 函數(shù)從協(xié)程外部發(fā)出值。
在內(nèi)部,callbackFlow
使用一個(gè) channel,它在概念上與阻塞 queue 非常相似。 一個(gè)通道配置了一個(gè)容量,即可以緩沖的最大元素?cái)?shù)。 在 callbackFlow
中創(chuàng)建的通道默認(rèn)容量為 64 個(gè)元素。 當(dāng)您嘗試將新元素添加到完整頻道時(shí),發(fā)送會(huì)暫停生產(chǎn)者,直到有新元素的空間,而 offer
不會(huì)將元素添加到頻道并立即返回 false
。
額外 Flow 資料鏈接: