響應(yīng)式的框架
RxJava:過于復(fù)雜、學(xué)習(xí)成本高
LiveData:針對Android定制、使用簡單
針對Java開發(fā)者,初學(xué)者、簡單場景可以考慮使用LiveData。除此以外,可以考慮使用Kotlin Flows。但是Kotlin Flows現(xiàn)在依然有陡峭的學(xué)習(xí)曲線,但它是Kotlin語言的一部分,由Jetbrains提供支持;另外即將到來的Jetpack Compose 非常適合響應(yīng)式模式。
Flow:簡單的事情更難,復(fù)雜的事情更容易
LiveData擅長于暴露最近獲取的數(shù)據(jù),并且能夠結(jié)合Android的生命周期。后來我們了解到它也可以啟動協(xié)程并創(chuàng)建復(fù)雜的轉(zhuǎn)換,但這有點復(fù)雜。
現(xiàn)在讓我們看看一些 LiveData 模式和它們的 Flow 等價寫法:
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
}
}
}
我們可以使用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:
- 總是有值
- 只有一個值
- 支持多個訂閱者
- 總是重播訂閱的最新值,與活躍觀察者的數(shù)量無關(guān)
向視圖公開 UI 狀態(tài)時,請使用 StateFlow。 它是一個安全高效的觀察者,旨在保持 UI 狀態(tài)。
2、公開一次性操作的結(jié)果
這與前面的代碼片段等效,公開了沒有可變后備屬性的協(xié)程調(diào)用的結(jié)果。
對于 LiveData,我們?yōu)榇耸褂昧?liveData 協(xié)程構(gòu)建器:

class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于狀態(tài)持有者總是有一個值,因此最好將我們的 UI 狀態(tài)包裝在某種支持加載、成功和錯誤等狀態(tài)的 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 轉(zhuǎn)換為 StateFlow 的 Flow 運算符。 現(xiàn)在讓我們相信這些參數(shù),因為我們稍后需要更多的復(fù)雜性來正確解釋它。
3、帶參數(shù)的一次性數(shù)據(jù)加載
假設(shè)您想加載一些依賴于用戶 ID 的數(shù)據(jù),并且您從 AuthManager 的公開的flow獲取此信息:

使用 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 是一個轉(zhuǎn)換,它的主體被執(zhí)行,并且當 userId 改變時訂閱結(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
)
}
請注意,如果您需要更大的靈活性,您還可以使用 transformLatest 并顯式發(fā)出數(shù)據(jù)項:
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
)
5、觀察帶參數(shù)的數(shù)據(jù)流
現(xiàn)在讓我們讓這個更具響應(yīng)性的例子。 數(shù)據(jù)不是獲取的,而是觀察到的,因此我們將數(shù)據(jù)源中的更改自動傳播到 UI。
繼續(xù)我們的例子:我們沒有在數(shù)據(jù)源上調(diào)用 fetchItem,而是使用一個假設(shè)的 observeItem 操作符,它返回一個 Flow。
使用 LiveData,您可以將流轉(zhuǎn)換為 LiveData 并發(fā)出所有更新:

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 組合兩個流,并僅將輸出轉(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 實現(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
)
}
每當用戶更改或存儲庫中的用戶數(shù)據(jù)更改時,公開的 StateFlow 都會收到更新。
5、組合多個來源:MediatorLiveData -> Flow.combine
MediatorLiveData 可讓您觀察一個或多個更新源(LiveData 可觀察對象)并在它們獲得新數(shù)據(jù)時執(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 等價寫法更直接:
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)在不想詳細介紹,只需要復(fù)制粘貼,我推薦這種組合:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
但是,如果您不確定這個看似隨機的 5 秒啟動參數(shù),請繼續(xù)閱讀。
stateIn 有 3 個參數(shù)(來自文檔):
@param scope 開始共享的協(xié)程范圍。
@param 啟動了控制何時開始和停止共享的策略。
@param initialValue 狀態(tài)流的初始值。
當使用帶有 replayExpirationMillis 參數(shù)的 [SharingStarted.WhileSubscribed] 策略重置狀態(tài)流時,也會使用此值。
開始可以采用 3 個值
- Lazily:在第一個訂閱者出現(xiàn)時開始,在作用域取消時停止。
- Eagerly:立即開始并在作用域取消時停止
- WhileSubscribed:這很復(fù)雜。
對于一次性操作,您可以使用 Lazily 或 Eagerly。 但是,如果您正在觀察其他流程,則應(yīng)該使用 WhileSubscribed 來執(zhí)行小而重要的優(yōu)化,如下所述。
WhileSubscribed 策略
WhileSubscribed 在沒有收集器時取消上游流。 使用 stateIn 創(chuàng)建的 StateFlow 向 View 公開數(shù)據(jù),但它也在觀察來自其他層或應(yīng)用程序(上游)的流。保持這些流處于活動狀態(tài)可能會導(dǎo)致資源浪費,例如,如果它們繼續(xù)從其他來源(如數(shù)據(jù)庫連接、硬件傳感器等)讀取數(shù)據(jù)。當你的應(yīng)用進入后臺時,你應(yīng)該做一個好公民并停止這些協(xié)程。
WhileSubscribed 有兩個參數(shù):
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
Stop timeout
從它的文檔:
stopTimeoutMillis配置最后一個訂閱者消失和上游流停止之間的延遲(以毫秒為單位)。 它默認為零(立即停止)。
這很有用,因為如果視圖停止偵聽幾分之一秒,您不想取消上游流。 這一直發(fā)生——例如,當用戶旋轉(zhuǎn)設(shè)備并且視圖被快速連續(xù)地破壞和重新創(chuàng)建時。
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
)
}
這種方法檢查所有框:
- 當用戶將您的應(yīng)用程序發(fā)送到后臺時,來自其他層的更新將在 5 秒后停止,從而節(jié)省電量。
- 最新的值仍然會被緩存,這樣當用戶回到它時,視圖會立即有一些數(shù)據(jù)。
- 訂閱重新啟動,新值將出現(xiàn),可用時刷新屏幕。
重播到期
如果您不希望用戶在他們離開太久后看到陳舊數(shù)據(jù)并且您更喜歡顯示加載屏幕,請查看 WhileSubscribed 中的 replayExpirationMillis 參數(shù)。 在這種情況下它非常方便,而且還節(jié)省了一些內(nèi)存,因為緩存的值會恢復(fù)到 stateIn 中定義的初始值。 返回應(yīng)用程序不會那么快,但您不會顯示舊數(shù)據(jù)。
replayExpirationMillis——配置共享協(xié)程停止和重置重放緩存之間的延遲(以毫秒為單位)(這使得 shareIn 操作符的緩存為空,并將緩存值重置為 stateIn 操作符的原始初始值)。 它默認為 Long.MAX_VALUE(永遠保持重放緩存,從不重置緩沖區(qū))。 使用零值立即使緩存過期。
從視圖中觀察 StateFlow
到目前為止,我們已經(jīng)看到,讓 ViewModel 中的 StateFlows 知道View已經(jīng)不再監(jiān)聽是非常重要的。 然而,與生命周期相關(guān)的所有事情一樣,事情并沒有那么簡單。
為了收集流,您需要一個協(xié)程。 Activities和Fragments提供了一堆協(xié)程構(gòu)建器:
Activity.lifecycleScope.launch:立即啟動協(xié)程,活動銷毀時取消協(xié)程。Fragment.lifecycleScope.launch:立即啟動協(xié)程,并在片段銷毀時取消協(xié)程。
-
Fragment.viewLifecycleOwner.lifecycleScope.launch:立即啟動協(xié)程,并在片段的視圖生命周期被銷毀時取消協(xié)程。 如果您正在修改 UI,您應(yīng)該使用視圖生命周期。
LaunchWhenStarted, launchWhenResumed…
稱為launchWhenX 的特殊版本的launch 將等到lifecycleOwner 處于X 狀態(tài)并在lifecycleOwner 低于X 狀態(tài)時暫停協(xié)程。 重要的是要注意,在其生命周期所有者被銷毀之前,它們不會取消協(xié)程。

在應(yīng)用程序處于后臺時接收更新可能會導(dǎo)致崩潰,這可以通過暫停視圖中的集合來解決。 但是,當應(yīng)用程序在后臺時,上游流會保持活動狀態(tài),這可能會浪費資源。
這意味著到目前為止我們?yōu)榕渲?StateFlow 所做的一切都將毫無用處; 然而,現(xiàn)在有一個新的 API。
lifecycle.repeatOnLifecycle
這個新的協(xié)程構(gòu)建器(可從lifecycle-runtime-ktx 2.4.0-alpha01 獲得)正是我們所需要的:它在特定狀態(tài)下啟動協(xié)程,并在生命周期所有者低于它時停止它們。

例如,在一個Fragment中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
這將在 Fragment 的視圖開始時開始收集,將繼續(xù)通過 RESUMED,并在返回到 STOPPED 時停止。
點擊閱讀相關(guān)的全部介紹 A safer way to collect flows from Android UIs。
將 repeatOnLifecycle API 與上面的 StateFlow 指南結(jié)合在一起,可以在充分利用設(shè)備資源的同時獲得最佳性能。

警告:StateFlow support recently added to Data Binding 目前使用
*launchWhenCreated*來收集更新,在達到穩(wěn)定之后將會采用*repeatOnLifecycle*。對于數(shù)據(jù)綁定,您應(yīng)該在任何地方使用 Flows 并簡單地添加 asLiveData() 以將它們公開給視圖。 數(shù)據(jù)綁定將在 Lifecycle-runtime-ktx 2.4.0 穩(wěn)定后更新。
總結(jié)
從 ViewModel 公開數(shù)據(jù)并從視圖收集數(shù)據(jù)的最佳方法是:
任何其他組合都會使上游 Flows 保持活動狀態(tài),從而浪費資源:
- 使用
WhileSubscribed公開并在生命周期范圍內(nèi)使用launch/launchWhenX收集 - 使用
Lazily/Eagerly公開并使用repeatOnLifecycle收集
當然,如果您不需要 Flow 的全部功能……只需使用 LiveData。 :)