LiveData 的歷史要追溯到 2017 年。彼時,觀察者模式有效簡化了開發,但諸如 RxJava 一類的庫對新手而言有些太過複雜。爲此,架構組件團隊打造了 LiveData: 一個專用於 Android 的具有自主生命週期感知能力的可觀察的數據存儲器類。LiveData 被有意簡化設計,這使得開發者很容易上手;而對於較爲複雜的交互數據流場景,建議您使用 RxJava,這樣二者結合的優點就發揮出來了。html
LiveData 對於 Java 開發者、初學者或是一些簡單場景而言還是可行的解決方案。而對於一些其餘的場景,更好的選擇是使用 Kotlin 數據流 (Kotlin Flow)。雖然說數據流 (相較 LiveData) 有更陡峭的學習曲線,但因爲它是 JetBrains 力挺的 Kotlin 語言的一部分,且 Jetpack Compose 正式版即將發佈,故二者配合更能發揮出 Kotlin 數據流中響應式模型的潛力。java
此前一段時間,咱們探討了 如何使用 Kotlin 數據流 來鏈接您的應用當中除了視圖和 View Model 之外的其餘部分。而如今咱們有了 一種更安全的方式來從 Android 的界面中得到數據流,已經能夠創做一份完整的遷移指南了。react
在這篇文章中,您將學到如何把數據流暴露給視圖、如何收集數據流,以及如何經過調優來適應不一樣的需求。android
LiveData 就作了一件事而且作得不錯: 它在 緩存最新的數據 和感知 Android 中的生命週期的同時將數據暴露了出來。稍後咱們會了解到 LiveData 還能夠 啓動協程 和 建立複雜的數據轉換,這可能會須要花點時間。git
接下來咱們一塊兒比較 LiveData 和 Kotlin 數據流中相對應的寫法吧:github
#1: 使用可變數據存儲器暴露一次性操做的結果數據庫
這是一個經典的操做模式,其中您會使用協程的結果來改變狀態容器:緩存
△ 將一次性操做的結果暴露給可變的數據容器 (LiveData)安全
<!-- 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 // 從掛起函數和可變狀態中加載數據 init { viewModelScope.launch { val result = ... _myUiState.value = result } } }
若是要在 Kotlin 數據流中執行相同的操做,咱們須要使用 (可變的) StateFlow (狀態容器式可觀察數據流):架構
△ 使用可變數據存儲器 (StateFlow) 暴露一次性操做的結果
class MyViewModel { private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading) val myUiState: StateFlow<Result<UiState>> = _myUiState // 從掛起函數和可變狀態中加載數據 init { viewModelScope.launch { val result = ... _myUiState.value = result } } }
StateFlow 是 SharedFlow 的一個比較特殊的變種,而 SharedFlow 又是 Kotlin 數據流當中比較特殊的一種類型。StateFlow 與 LiveData 是最接近的,由於:
當暴露 UI 的狀態給視圖時,應該使用 StateFlow。這是一種安全和高效的觀察者,專門用於容納 UI 狀態。
#2: 把一次性操做的結果暴露出來
這個例子與上面代碼片斷的效果一致,只是這裏暴露協程調用的結果而無需使用可變屬性。
若是使用 LiveData,咱們須要使用 LiveData 協程構建器:
△ 把一次性操做的結果暴露出來 (LiveData)
class MyViewModel(...) : ViewModel() { val result: LiveData<Result<UiState>> = liveData { emit(Result.Loading) emit(repository.fetchItem()) } }
因爲狀態容器老是有值的,那麼咱們就能夠經過某種 Result 類來把 UI 狀態封裝起來,好比加載中、成功、錯誤等狀態。
與之對應的數據流方式則須要您多作一點配置:
△ 把一次性操做的結果暴露出來 (StateFlow)
class MyViewModel(...) : ViewModel() { val result: StateFlow<Result<UiState>> = flow { emit(repository.fetchItem()) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), //因爲是一次性操做,也可使用 Lazily initialValue = Result.Loading ) }
stateIn 是專門將數據流轉換爲 StateFlow 的運算符。因爲須要經過更復雜的示例才能更好地解釋它,因此這裏暫且把這些參數放在一邊。
#3: 帶參數的一次性數據加載
比方說您想要加載一些依賴用戶 ID 的數據,而信息來自一個提供數據流的 AuthManager:
△ 帶參數的一次性數據加載 (LiveData)
使用 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 變化時執行。
如非必需要將 userId
做爲 LiveData 使用,那麼更好的方案是將流式數據和 Flow 結合,並將最終的結果 (result) 轉化爲 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() }
若是改用 Kotlin Flow 來編寫,代碼其實似曾相識:
△ 帶參數的一次性數據加載 (StateFlow)
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 //注意此處不一樣的加載狀態 )
#4: 觀察帶參數的數據流
接下來咱們讓剛纔的案例變得更具交互性。數據再也不被讀取,而是被觀察,所以咱們對數據源的改動會直接被傳遞到 UI 界面中。
繼續剛纔的例子: 咱們再也不對源數據調用 fetchItem 方法,而是經過假定的 observeItem 方法獲取一個 Kotlin 數據流。
若使用 LiveData,能夠將數據流轉換爲 LiveData 實例,而後經過 emitSource 傳遞數據的變化。
△ 觀察帶參數的數據流 (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() }
使用 Kotlin 數據流的實現方式很是類似,可是省下了 LiveData 的轉換過程:
△ 觀察帶參數的數據流 (StateFlow)
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 ) }
每當用戶實例變化,或者是存儲區 (repository) 中用戶的數據發生變化時,上面代碼中暴露出來的 StateFlow 都會收到相應的更新信息。
#5: 結合多種源: MediatorLiveData -> Flow.combine
MediatorLiveData 容許您觀察一個或多個數據源的變化狀況,並根據獲得的新數據進行相應的操做。一般能夠按照下面的方式更新 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)) }
一樣的功能使用 Kotlin 數據流來操做會更加直接:
val flow1: Flow<Int> = ... val flow2: Flow<Int> = ... val result = combine(flow1, flow2) { a, b -> a + b }
此處也可使用 combineTransform 或者 zip 函數。
早前咱們使用 stateIn
中間運算符來把普通的流轉換成 StateFlow,但轉換以後還須要一些配置工做。若是如今不想了解太多細節,只是想知道怎麼用,那麼可使用下面的推薦配置:
val result: StateFlow<Result<UiState>> = someFlow .stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading )
不過,若是您想知道爲何會使用這個看似隨機的 5 秒的 started 參數,請繼續往下讀。
根據文檔,stateIn
有三個參數:
@param scope 共享開始時所在的協程做用域範圍 @param started 控制共享的開始和結束的策略 @param initialValue 狀態流的初始值 當使用 [SharingStarted.WhileSubscribed] 並帶有 `replayExpirationMillis` 參數重置狀態流時,也會用到 initialValue。
started
接受如下的三個值:
Lazily
: 當首個訂閱者出現時開始,在 scope
指定的做用域被結束時終止。Eagerly
: 當即開始,而在 scope
指定的做用域被結束時終止。WhileSubscribed
: 這種狀況有些複雜 (後文詳聊)。對於那些只執行一次的操做,您可使用 Lazily 或者 Eagerly。然而,若是您須要觀察其餘的流,就應該使用 WhileSubscribed 來實現細微但又重要的優化工做,參見後文的解答。
WhileSubscribed 策略會在沒有收集器的狀況下取消上游數據流。經過 stateIn 運算符建立的 StateFlow 會把數據暴露給視圖 (View),同時也會觀察來自其餘層級或者是上游應用的數據流。讓這些流持續活躍可能會引發沒必要要的資源浪費,例如一直經過從數據庫鏈接、硬件傳感器中讀取數據等等。當您的應用轉而在後臺運行時,您應當保持克制並停止這些協程。
WhileSubscribed
接受兩個參數:
public fun WhileSubscribed( stopTimeoutMillis: Long = 0, replayExpirationMillis: Long = Long.MAX_VALUE )
超時中止
根據其文檔:
stopTimeoutMillis 控制一個以毫秒爲單位的延遲值,指的是最後一個訂閱者結束訂閱與中止上游流的時間差。默認值是 0 (當即中止)。
這個值很是有用,由於您可能並不想由於視圖有幾秒鐘再也不監聽就結束上游流。這種狀況很是常見——好比當用戶旋轉設備時,原來的視圖會先被銷燬,而後數秒鐘內重建。
liveData 協程構建器所使用的方法是 添加一個 5 秒鐘的延遲,即若是等待 5 秒後仍然沒有訂閱者存在就終止協程。前文代碼中的 WhileSubscribed (5000) 正是實現這樣的功能:
class MyViewModel(...) : ViewModel() { val result = userId.mapLatest { newUserId -> repository.observeItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading ) }
這種方法會在如下場景獲得體現:
數據重現的過時時間
若是用戶離開應用過久,此時您不想讓用戶看到陳舊的數據,而且但願顯示數據正在加載中,那麼就應該在 WhileSubscribed 策略中使用 replayExpirationMillis 參數。在這種狀況下此參數很是適合,因爲緩存的數據都恢復成了 stateIn 中定義的初始值,所以能夠有效節省內存。雖然用戶切迴應用時可能沒那麼快顯示有效數據,但至少不會把過時的信息顯示出來。
replayExpirationMillis
配置了以毫秒爲單位的延遲時間,定義了從中止共享協程到重置緩存 (恢復到 stateIn 運算符中定義的初始值 initialValue) 所須要等待的時間。它的默認值是長整型的最大值 Long.MAX_VALUE (表示永遠不將其重置)。若是設置爲 0,能夠在符合條件時當即重置緩存的數據。
咱們此前已經談到,ViewModel 中的 StateFlow 須要知道它們已經再也不須要監聽。然而,當全部的這些內容都與生命週期 (lifecycle) 結合起來,事情就沒那麼簡單了。
要收集一個數據流,就須要用到協程。Activity 和 Fragment 提供了若干協程構建器:
對於一個狀態 X,有專門的 launch 方法稱爲 launchWhenX。它會在 lifecycleOwner 進入 X 狀態以前一直等待,又在離開 X 狀態時掛起協程。對此,須要注意對應的協程只有在它們的生命週期全部者被銷燬時纔會被取消。
△ 使用 launch/launchWhenX 來收集數據流是不安全的
當應用在後臺運行時接收數據更新可能會引發應用崩潰,但這種狀況能夠經過將視圖的數據流收集操做掛起來解決。然而,上游數據流會在應用後臺運行期間保持活躍,所以可能浪費必定的資源。
這麼說來,目前咱們對 StateFlow 所進行的配置都是無用功;不過,如今有了一個新的 API。
這個新的協程構建器 (自 lifecycle-runtime-ktx 2.4.0-alpha01 後可用) 剛好能知足咱們的須要: 在某個特定的狀態知足時啓動協程,而且在生命週期全部者退出該狀態時中止協程。
△ 不一樣數據流收集方法的比較
好比在某個 Fragment 的代碼中:
onCreateView(...) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) { myViewModel.myUiState.collect { ... } } } }
當這個 Fragment 處於 STARTED 狀態時會開始收集流,而且在 RESUMED 狀態時保持收集,最終在 Fragment 進入 STOPPED 狀態時結束收集過程。如需獲取更多信息,請參閱: 使用更爲安全的方式收集 Android UI 數據流。
結合使用 repeatOnLifecycle API 和上面的 StateFlow 示例能夠幫助您的應用妥善利用設備資源的同時,發揮最佳性能。
△ 該 StateFlow 經過 WhileSubscribed(5000) 暴露並經過 repeatOnLifecycle(STARTED) 收集
注意: 近期在 Data Binding 中加入的 StateFlow 支持 使用了
launchWhenCreated
來描述收集數據更新,而且它會在進入穩定版後轉而使用repeatOnLifecyle
。對於數據綁定,您應該在各處都使用 Kotlin 數據流並簡單地加上
asLiveData()
來把數據暴露給視圖。數據綁定會在lifecycle-runtime-ktx 2.4.0
進入穩定版後更新。
經過 ViewModel 暴露數據,並在視圖中獲取的最佳方式是:
若是採用其餘方式,上游數據流會被一直保持活躍,致使資源浪費:
WhileSubscribed
暴露 StateFlow,而後在 lifecycleScope.launch/launchWhenX
中收集數據更新。Lazily/Eagerly
策略暴露 StateFlow,並在 repeatOnLifecycle
中收集數據更新。固然,若是您並不須要使用到 Kotlin 數據流的強大功能,就用 LiveData 好了 :)