【譯】LiveData with Coroutines and Flow
點擊上方藍字關(guān)注我,知識會給你力量
這個系列我做了協(xié)程和Flow開發(fā)者的一系列文章的翻譯,旨在了解當前協(xié)程、Flow、LiveData這樣設(shè)計的原因,從設(shè)計者的角度,發(fā)現(xiàn)他們的問題,以及如何解決這些問題,pls enjoy it。
Part I: Reactive UIs
從Android的早期開始,我們就很快了解到Android的生命周期很難理解,充滿了邊緣案例,而保持理智的最好方法就是盡可能地避免它們。
為此,我們建議采用分層架構(gòu),這樣我們就可以編寫?yīng)毩⒂赨I的代碼,而不用過多考慮生命周期。例如,我們可以添加一個持有業(yè)務(wù)邏輯的領(lǐng)域?qū)樱愕膽?yīng)用程序?qū)嶋H做什么)和一個數(shù)據(jù)層。
此外,我們了解到表現(xiàn)層可以被分成不同的組件,承擔不同的責(zé)任。
-
View--處理生命周期的回調(diào)、用戶事件和Activity或Fragment的導(dǎo)航 -
Presenter、ViewModel--為View提供數(shù)據(jù),并且大多不知道在View中進行的生命周期。這意味著沒有中斷,也不需要在重新創(chuàng)建視圖時進行清理。
撇開命名不談,有兩種機制可以將數(shù)據(jù)從ViewModel/Presenter發(fā)送到View。
-
擁有對視圖的引用并直接調(diào)用它。通常與Presenters的工作方式有關(guān)。 -
將可觀察的數(shù)據(jù)暴露給觀察者。通常與ViewModels的工作方式有關(guān)。
這是一個在Android社區(qū)相當成熟的慣例,但你會發(fā)現(xiàn)有一些文章有不同意見。有數(shù)百篇博客文章以不同的方式定義Presenter、ViewModel、MVP和MVVM。我的建議是,你專注于你的表現(xiàn)層的特性,使用Android架構(gòu)組件ViewModel。
-
在配置變化中保存下來,如旋轉(zhuǎn)、地域變化、窗口大小調(diào)整、黑暗模式切換等。 -
有一個非常簡單的生命周期。它有一個單一的生命周期回調(diào),onCleared,一旦它的生命周期所有者完成,就會被調(diào)用。
ViewModel被設(shè)計為使用觀察者模式來使用。
-
它不應(yīng)該有對視圖的引用。 -
它將數(shù)據(jù)暴露給觀察者,但不知道這些觀察者是什么。你可以使用LiveData來實現(xiàn)這一點。
當一個視圖(一個Activity、Fragment或任何生命周期的所有者)被創(chuàng)建時,ViewModel被獲得,它開始通過一個或多個LiveDatas暴露數(shù)據(jù),而視圖訂閱了這些數(shù)據(jù)。
這個訂閱可以用LiveData.observe設(shè)置,也可以用Data Binding庫自動設(shè)置。
現(xiàn)在,如果設(shè)備被旋轉(zhuǎn),那么視圖將被銷毀(#1),并創(chuàng)建一個新的實例(#2)。
如果我們在ViewModel中有一個對Activity的引用,我們將需要確保。
-
當視圖被銷毀時清除它 -
如果視圖處于transitional狀態(tài),避免訪問。
但有了ViewModel+LiveData,我們就不必再處理這個問題了。這就是為什么我們在《應(yīng)用程序架構(gòu)指南》中推薦這種方法。
Scopes
由于Activities和Fragments比ViewModels有相等或更短的壽命,我們可以開始討論操作的范圍了。
操作是你在應(yīng)用中需要做的任何事情,比如從網(wǎng)絡(luò)上獲取數(shù)據(jù)、過濾結(jié)果或計算一些文本的排列。
對于你創(chuàng)建的任何操作,你需要考慮其范圍:從啟動到取消的時間范圍。讓我們看兩個例子。
-
你在一個Activity的onStart中啟動一個操作,你在onStop中停止它。 -
你在ViewModel的initblock中啟動一個操作,然后在onCleared()中停止它。
看一下這個圖,我們可以找到每個操作的意義所在。
-
在一個作用于Activity的操作中獲取數(shù)據(jù)操作,將迫使我們在旋轉(zhuǎn)后再次獲取它,所以它應(yīng)該被作用于ViewModel。 -
而排列文本在作用于ViewModel的操作中是沒有意義的,因為在旋轉(zhuǎn)之后,你的文本容器可能已經(jīng)改變了形狀。
顯然,現(xiàn)實世界中的應(yīng)用可以有比這些更多的作用域。例如,在Android Dev Summit應(yīng)用程序中,我們可以使用。
-
Fragment scopes,每個屏幕有多個 -
Fragment ViewModel作用域,每屏一個 -
Main Activity scopes -
Main Activity ViewModel scope -
Application scope
這可能會產(chǎn)生很多不同的作用域,所以管理所有的作用域會讓人不知所措。我們需要一種方法來結(jié)構(gòu)化這種并發(fā)性!
一個非常方便的解決方案是Kotlin Coroutines。
我們喜歡在Android中使用Coroutines有很多原因。其中一些是。
-
很容易脫離主線程。Android應(yīng)用為了獲得流暢的用戶體驗而不斷地在線程間切換,而Coroutines讓這一切變得超級簡單。 -
有最小的代碼模板。Coroutines被嵌入到語言中,所以使用諸如suspend功能的東西是很容易的。 -
結(jié)構(gòu)化的并發(fā)性。這意味著你不得不定義你的操作范圍,而且你可以享受一些代碼層面的保證,從而消除大量的模板代碼,如清理代碼等。你可以把結(jié)構(gòu)化并發(fā)想象成“自動取消”。
如果你想了解coroutines的介紹,可以看看Android的介紹和Kotlin的官方文檔。
Part II: Launching coroutines with Architecture Components
Jetpack的架構(gòu)組件提供了一堆語法糖,所以你不必擔心Jobs和它們的取消行為。你只需要選擇你的操作范圍。
ViewModel scope
這是啟動coroutine最常見的方式之一,因為大多數(shù)數(shù)據(jù)操作都是從ViewModel開始的。使用viewModelScope擴展,當ViewModel被清除時,Job會自動取消。使用viewModelScope. launch來啟動coroutine。
class MainActivityViewModel : ViewModel {
init {
viewModelScope.launch {
// Do things!
}
}
}
Activity and Fragment scopes
同樣,如果你使用lifecycleScope.launch,你可以將操作的范圍限定在一個視圖的特定實例上。
如果你用launchWhenResumed、launchWhenStarted或launchWhenCreated,則會將操作限制在某一生命周期狀態(tài),你甚至可以有一個更窄的范圍。
class MyActivity : Activity {
override fun onCreate(state: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Run
}
lifecycleScope.launchWhenResumed {
// Run
}
}
}
Application scope
全應(yīng)用程序范圍有很好的用例:
https://medium.com/androiddevelopers/coroutines-patterns-for-work-that-shouldnt-be-cancelled-e26c40f142ad
但是首先,如果你的代碼最終必須被執(zhí)行,你應(yīng)該考慮使用WorkManager。
ViewModel + LiveData
到目前為止,我們已經(jīng)看到了如何啟動一個coroutine,但沒有看到如何從它那里接收一個結(jié)果。你可以像這樣使用一個MutableLiveData。
// Don't do this. Use liveData instead.
class MyViewModel : ViewModel() {
private val _result = MutableLiveData<String>()
val result: LiveData<String> = _result
init {
viewModelScope.launch {
val computationResult = doComputation()
_result.value = computationResult
}
}
}
但是,由于你將把這個結(jié)果暴露給你的視圖,你可以通過使用liveData coroutine builder來節(jié)省一些模板代碼,它可以啟動一個coroutine,讓你通過一個不可變的LiveData來暴露結(jié)果。你可以使用emit()來向它發(fā)送更新。
class MyViewModel : ViewModel() {
val result = liveData {
emit(doComputation())
}
}
LiveData Coroutine builder with a switchMap
在某些情況下,只要LiveData的值發(fā)生變化,你就想啟動一個coroutine。例如,當你在開始數(shù)據(jù)加載操作之前,你需要一個ID參數(shù)。有一個方便的模式,那就是使用Transformations.switchMap。
private val itemId = MutableLiveData<String>()
val result = itemId.switchMap {
liveData { emit(fetchItem(it)) }
}
result是一個不可變的LiveData,只要itemId有新的值,就會用調(diào)用fetchItem suspend函數(shù)的結(jié)果來更新數(shù)據(jù)。
Emit all items from another LiveData
這個功能不太常見,但也可以節(jié)省一些模板代碼:你可以使用emitSource傳遞一個LiveData數(shù)據(jù)源。當你想先發(fā)射一個初始值,然后再發(fā)射一連串的值時,這很有用。
liveData(Dispatchers.IO) {
emit(LOADING_STRING)
emitSource(dataSource.fetchWeather())
}
Cancelling coroutines
如果你使用上面的任何一種模式,你就不必明確地取消Job。然而,有一件重要的事情要記?。篶oroutine的取消是協(xié)作式的。
這意味著,如果調(diào)用的coroutine被取消了,你必須幫助Kotlin停止一個Job。比方說,你有一個啟動無限循環(huán)的suspend函數(shù)。Kotlin沒有辦法為你停止這個循環(huán),所以你需要合作,定期檢查這個Job是否在活動狀態(tài)。你可以通過檢查isActive屬性來做到這一點。
suspend fun printPrimes() {
while(isActive) {
// Compute
}
}
順便說一下,如果你使用kotlinx.coroutines中的任何函數(shù)(如delay),你應(yīng)該知道它們都是可取消的,這意味著它們會為你做這種檢查。
suspend fun printPrimes() {
while(true) { // Ok-ish because we call delay inside
// Compute
delay(1000)
}
}
也就是說,我建議你無論如何都要添加這個檢查,因為將來可能會有人刪除這個延遲調(diào)用,在你的代碼中引入一個微妙的錯誤。
One-shot vs multiple values
為了理解coroutines(以及反應(yīng)式UI),我們需要對以下內(nèi)容進行重要區(qū)分。
-
One-shot操作。它們只運行一次,可以返回一個結(jié)果 -
返回多個值的操作。對一個數(shù)據(jù)源的訂閱,可以在一段時間內(nèi)發(fā)出多個值
One-shot operations with coroutines
使用suspend函數(shù)并使用viewModelScope或liveData{}調(diào)用它們是運行非阻塞操作的一種非常方便的方法。
class MyViewModel {
val result = liveData {
emit(repository.fetchData())
}
}
然而,當我們在監(jiān)聽變化時,事情就變得有點復(fù)雜了。
Receiving multiple values with LiveData
我在《LiveData beyond the ViewModel》(2018)中談到了這個話題,在那里我談到了,LiveData從未被設(shè)計成一個功能齊全的流構(gòu)建器這一事實。
https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
現(xiàn)在,更好的方法是使用Kotlin的Flow(警告:有些部分仍在試驗中)。Flow類似于RxJava中的反應(yīng)式流功能。
然而,雖然輪子讓非阻塞的一次性操作變得更容易,但這對Flow來說并不是同樣的情況。Flow仍然是難以掌握的。不過,如果你想創(chuàng)建快速而可靠的反應(yīng)式UI,我認為值得花時間來學(xué)習(xí)。由于它是語言的一部分,而且是一個小的依賴項,許多庫都開始添加Flow支持(比如Room)。
因此,我們可以從數(shù)據(jù)源和存儲庫中暴露Flow,而不是LiveData,但ViewModel仍然暴露LiveData,因為它是生命周期感知的。
Part III: LiveData and coroutines patterns
ViewModel patterns
讓我們看看一些可用于ViewModels的模式,比較一下LiveData和Flow的使用。
LiveData: Emit N values as LiveData
val currentWeather: LiveData<String> = dataSource.fetchWeather()
如果我們不做任何轉(zhuǎn)換,我們可以簡單地將一個分配給另一個。
Flow: Emit N values as LiveData
我們可以使用liveData coroutine builder和Flow上的collect(這是一個接收每個發(fā)射值的終端操作符)的組合。
// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
dataSource.fetchWeatherFlow().collect {
emit(it)
}
}
但由于它有很多模板代碼,所以我們添加了Flow.asLiveData()擴展函數(shù),它可以在一行中做同樣的事情。
val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()
LiveData: Emit 1 initial value + N values from data source
如果數(shù)據(jù)源暴露了一個LiveData,我們可以使用emitSource在用emit發(fā)射一個初始值后進行批量更新。
val currentWeather: LiveData<String> = liveData {
emit(LOADING_STRING)
emitSource(dataSource.fetchWeather())
}
Flow: Emit 1 initial value + N values from data source
同樣,我們可以天真地做到這一點。
// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
emit(LOADING_STRING)
emitSource(
dataSource.fetchWeatherFlow().asLiveData()
)
}
但如果我們利用Flow自己的API,事情看起來就會整潔很多。
val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.onStart { emit(LOADING_STRING) }
.asLiveData()
onStart設(shè)置初始值,這樣做我們只需要向LiveData轉(zhuǎn)換一次。
LiveData: Suspend transformation
比方說,你想對來自數(shù)據(jù)源的東西進行轉(zhuǎn)換,但它可能是CPU密集型的,所以它是在一個suspend函數(shù)中。
你可以在數(shù)據(jù)源的LiveData上使用switchMap,然后用LiveData生成器創(chuàng)建coroutine?,F(xiàn)在你只需對收到的每個結(jié)果調(diào)用emit即可。
val currentWeatherLiveData: LiveData<String> =
dataSource.fetchWeather().switchMap {
liveData { emit(heavyTransformation(it)) }
}
Flow: Suspend transformation
這就是Flow與LiveData相比真正的優(yōu)勢所在。我們可以再次使用Flow的API來更優(yōu)雅地做事情。在這種情況下,我們使用Flow.map來在每次更新時應(yīng)用轉(zhuǎn)換。這一次,由于我們已經(jīng)在一個coroutine上下文中,我們可以直接調(diào)用它。
val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.map { heavyTransformation(it) }
.asLiveData()
Repository patterns
關(guān)于資源庫沒有什么好說的,因為如果你在使用Flow,你只需要使用Flow的API來轉(zhuǎn)換和組合數(shù)據(jù)。
val currentWeatherFlow: Flow<String> =
dataSource.fetchWeatherFlow()
.map { ... }
.filter { ... }
.dropWhile { ... }
.combine { ... }
.flowOn(Dispatchers.IO)
.onCompletion { ... }
Data source patterns
再次,讓我們區(qū)分一下One-shot場景和Flow。
One-shot operations in the data source
如果你正在使用一個支持suspend函數(shù)的庫,如Room或Retrofit,你可以簡單地從你的suspend函數(shù)中使用它們。
suspend fun doOneShot(param: String) : String = retrofitClient.doSomething(param)
然而,有些工具和庫還不支持coroutine,而是基于回調(diào)。
在這種情況下,你可以使用suspendCoroutine或suspendCancellableCoroutine。
(我不知道你為什么要使用不可取消的版本,但請在評論中告訴我!)
suspend fun doOneShot(param: String) : Result<String> =
suspendCancellableCoroutine { continuation ->
api.addOnCompleteListener { result ->
continuation.resume(result)
}.addOnFailureListener { error ->
continuation.resumeWithException(error)
}.fetchSomething(param)
}
當你調(diào)用它時,你會得到一個continuation。在這個例子中,我們使用的API讓我們設(shè)置了一個完成的監(jiān)聽器和一個失敗的監(jiān)聽器,所以在它們的回調(diào)中,當我們收到數(shù)據(jù)或錯誤時,我們會調(diào)用continuation.resume或continuation.resumeWithException。
值得注意的是,如果這個coroutine被取消,resume將被忽略,所以如果你的請求需要很長的時間,這個coroutine將處于活動狀態(tài),直到其中一個回調(diào)被執(zhí)行。
Exposing Flow in the data source
Flow builder
如果你需要創(chuàng)建一個假的數(shù)據(jù)源的實現(xiàn),或者你只是需要一些簡單的東西,你可以使用flow構(gòu)造器,做一些類似的事情。
override fun fetchWeatherFlow(): Flow<String> = flow {
var counter = 0
while(true) {
counter++
delay(2000)
emit(weatherConditions[counter % weatherConditions.size])
}
}
這段代碼每隔兩秒就會發(fā)出一個天氣狀況。
Callback-based APIs
如果你想把基于回調(diào)的API轉(zhuǎn)換為Flow,你可以使用callbackFlow。
fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
val callback = object : Callback {
override fun onNextValue(value: T) {
offer(value)
}
override fun onApiError(cause: Throwable) {
close(cause)
}
override fun onCompleted() = close()
}
api.register(callback)
awaitClose { api.unregister(callback) }
}
它看起來令人生畏,但如果你把它拆開,你會發(fā)現(xiàn)它有很大的意義。
-
當我們有一個新的Value時,我們調(diào)用offer方法 -
當我們想停止發(fā)送更新時,我們調(diào)用close(cause?) -
我們使用awaitClose來定義流程關(guān)閉時需要執(zhí)行的內(nèi)容,這對于取消注冊回調(diào)來說是非常完美的。
總之,coroutines和Flow將繼續(xù)存在。但它們并不能在所有地方取代LiveData。即使是非常有前途的StateFlow(目前是實驗性的),我們?nèi)匀挥蠮ava編程語言和DataBinding的用戶需要支持,所以它在一段時間內(nèi)不會被廢棄 :)
原文鏈接:https://medium.com/androiddevelopers/livedata-with-coroutines-and-flow-part-i-reactive-uis-b20f676d25d7
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
