響應式的框架
RxJava:過于復雜、學習成本高
LiveData:針對Android定制、使用簡單
針對Java開發者,初學者、簡單場景可以考慮使用LiveData。除此以外,可以考慮使用Kotlin Flows。但是Kotlin Flows現在依然有陡峭的學習曲線,但它是Kotlin語言的一部分,由Jetbrains提供支持;另外即將到來的Jetpack Compose 非常適合響應式模式。
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
}
}
}
我們可以使用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:
- 總是有值
- 只有一個值
- 支持多個訂閱者
- 總是重播訂閱的最新值,與活躍觀察者的數量無關
向視圖公開 UI 狀態時,請使用 StateFlow。 它是一個安全高效的觀察者,旨在保持 UI 狀態。
2、公開一次性操作的結果
這與前面的代碼片段等效,公開了沒有可變后備屬性的協程調用的結果。
對于 LiveData,我們為此使用了 liveData 協程構建器:
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于狀態持有者總是有一個值,因此最好將我們的 UI 狀態包裝在某種支持加載、成功和錯誤等狀態的 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 的公開的flow獲取此信息:
使用 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 并顯式發出數據項:
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、觀察帶參數的數據流
現在讓我們讓這個更具響應性的例子。 數據不是獲取的,而是觀察到的,因此我們將數據源中的更改自動傳播到 UI。
繼續我們的例子:我們沒有在數據源上調用 fetchItem,而是使用一個假設的 observeItem 操作符,它返回一個 Flow。
使用 LiveData,您可以將流轉換為 LiveData 并發出所有更新:
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 秒啟動參數,請繼續閱讀。
stateIn 有 3 個參數(來自文檔):
@param scope 開始共享的協程范圍。
@param 啟動了控制何時開始和停止共享的策略。
@param initialValue 狀態流的初始值。
當使用帶有 replayExpirationMillis
參數的 [SharingStarted.WhileSubscribed] 策略重置狀態流時,也會使用此值。
開始可以采用 3 個值
- Lazily:在第一個訂閱者出現時開始,在作用域取消時停止。
- Eagerly:立即開始并在作用域取消時停止
- WhileSubscribed:這很復雜。
對于一次性操作,您可以使用 Lazily 或 Eagerly。 但是,如果您正在觀察其他流程,則應該使用 WhileSubscribed 來執行小而重要的優化,如下所述。
WhileSubscribed 策略
WhileSubscribed 在沒有收集器時取消上游流。 使用 stateIn 創建的 StateFlow 向 View 公開數據,但它也在觀察來自其他層或應用程序(上游)的流。保持這些流處于活動狀態可能會導致資源浪費,例如,如果它們繼續從其他來源(如數據庫連接、硬件傳感器等)讀取數據。當你的應用進入后臺時,你應該做一個好公民并停止這些協程。
WhileSubscribed 有兩個參數:
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
Stop timeout
從它的文檔:
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 秒后停止,從而節省電量。
- 最新的值仍然會被緩存,這樣當用戶回到它時,視圖會立即有一些數據。
- 訂閱重新啟動,新值將出現,可用時刷新屏幕。
重播到期
如果您不希望用戶在他們離開太久后看到陳舊數據并且您更喜歡顯示加載屏幕,請查看 WhileSubscribed
中的 replayExpirationMillis
參數。 在這種情況下它非常方便,而且還節省了一些內存,因為緩存的值會恢復到 stateIn
中定義的初始值。 返回應用程序不會那么快,但您不會顯示舊數據。
replayExpirationMillis
——配置共享協程停止和重置重放緩存之間的延遲(以毫秒為單位)(這使得 shareIn 操作符的緩存為空,并將緩存值重置為 stateIn 操作符的原始初始值)。 它默認為 Long.MAX_VALUE(永遠保持重放緩存,從不重置緩沖區)。 使用零值立即使緩存過期。
從視圖中觀察 StateFlow
到目前為止,我們已經看到,讓 ViewModel 中的 StateFlows 知道View已經不再監聽是非常重要的。 然而,與生命周期相關的所有事情一樣,事情并沒有那么簡單。
為了收集流,您需要一個協程。 Activities和Fragments提供了一堆協程構建器:
Activity.lifecycleScope.launch:
立即啟動協程,活動銷毀時取消協程。Fragment.lifecycleScope.launch:
立即啟動協程,并在片段銷毀時取消協程。
-
Fragment.viewLifecycleOwner.lifecycleScope.launch:
立即啟動協程,并在片段的視圖生命周期被銷毀時取消協程。 如果您正在修改 UI,您應該使用視圖生命周期。
LaunchWhenStarted, launchWhenResumed…
稱為launchWhenX 的特殊版本的launch 將等到lifecycleOwner 處于X 狀態并在lifecycleOwner 低于X 狀態時暫停協程。 重要的是要注意,在其生命周期所有者被銷毀之前,它們不會取消協程
。
在應用程序處于后臺時接收更新可能會導致崩潰,這可以通過暫停視圖中的集合來解決。 但是,當應用程序在后臺時,上游流會保持活動狀態,這可能會浪費資源。
這意味著到目前為止我們為配置 StateFlow 所做的一切都將毫無用處; 然而,現在有一個新的 API。
lifecycle.repeatOnLifecycle
這個新的協程構建器(可從lifecycle-runtime-ktx 2.4.0-alpha01 獲得)正是我們所需要的:它在特定狀態下啟動協程,并在生命周期所有者低于它時停止它們。
例如,在一個Fragment中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
這將在 Fragment 的視圖開始時開始收集,將繼續通過 RESUMED,并在返回到 STOPPED 時停止。
點擊閱讀相關的全部介紹 A safer way to collect flows from Android UIs。
將 repeatOnLifecycle API 與上面的 StateFlow 指南結合在一起,可以在充分利用設備資源的同時獲得最佳性能。
警告:StateFlow support recently added to Data Binding 目前使用
*launchWhenCreated*
來收集更新,在達到穩定之后將會采用*repeatOnLifecycle*
。對于數據綁定,您應該在任何地方使用 Flows 并簡單地添加 asLiveData() 以將它們公開給視圖。 數據綁定將在 Lifecycle-runtime-ktx 2.4.0 穩定后更新。
總結
從 ViewModel 公開數據并從視圖收集數據的最佳方法是:
任何其他組合都會使上游 Flows 保持活動狀態,從而浪費資源:
- 使用
WhileSubscribed
公開并在生命周期范圍內使用launch/launchWhenX
收集 - 使用
Lazily/Eagerly
公開并使用repeatOnLifecycle
收集
當然,如果您不需要 Flow 的全部功能……只需使用 LiveData。 :)