LiveData beyond the ViewModel
點(diǎn)擊上方藍(lán)字關(guān)注我,知識會(huì)給你力量
這個(gè)系列我做了協(xié)程和Flow開發(fā)者的一系列文章的翻譯,旨在了解當(dāng)前協(xié)程、Flow、LiveData這樣設(shè)計(jì)的原因,從設(shè)計(jì)者的角度,發(fā)現(xiàn)他們的問題,以及如何解決這些問題,pls enjoy it。
多年來,反應(yīng)式架構(gòu)一直是Android的一個(gè)熱門話題。它一直是Android會(huì)議上的一個(gè)永恒主題,通常都是用RxJava的例子來進(jìn)行演示的(見底部的Rx部分)。反應(yīng)式編程是一種關(guān)注數(shù)據(jù)「如何流動(dòng)」以及「如何傳播」的范式,它可以簡化構(gòu)建應(yīng)用程序的代碼,方便顯示來自異步操作的數(shù)據(jù)。
實(shí)現(xiàn)一些反應(yīng)式概念的一個(gè)工具是LiveData。它是一個(gè)簡單的觀察者,能夠意識到觀察者的生命周期。從你的數(shù)據(jù)源或存儲庫中暴露LiveData是使你的架構(gòu)更具反應(yīng)性的一個(gè)簡單方法,但也有一些潛在的陷阱。
這篇博文將幫助你避免陷阱,并使用一些模式來幫助你使用LiveData構(gòu)建一個(gè)更加「反應(yīng)式」的架構(gòu)。
LiveData’s purpose
在Android中,Activity、Fragment和視圖幾乎可以在任何時(shí)候被銷毀,所以對這些組件之一的任何引用都可能導(dǎo)致泄漏或NullPointerException異常。
LiveData被設(shè)計(jì)用來實(shí)現(xiàn)觀察者模式,允許視圖控制器(Activity、Fragment等)和UI數(shù)據(jù)的來源(通常是ViewModel)之間進(jìn)行通信。
通過LiveData,這種通信更加安全:由于它的生命周期意識,數(shù)據(jù)只有在View處于Activity狀態(tài)時(shí)才會(huì)被接收。
簡而言之,其優(yōu)點(diǎn)是你不需要在View和ViewModel之間手動(dòng)取消訂閱。
LiveData beyond the ViewModel
可觀察范式在視圖控制器和ViewModel之間工作得非常好,所以你可以用它來觀察你的應(yīng)用程序的其他組件,并利用生命周期意識的優(yōu)勢。比如說下面這些場景:
-
觀察SharedPreferences中的變化 -
觀察Firestore中的一個(gè)文檔或集合 -
用FirebaseAuth這樣的認(rèn)證SDK觀察當(dāng)前用戶的授權(quán) -
觀察Room中的查詢(它支持開箱即用的LiveData)
這種模式的優(yōu)點(diǎn)是,由于所有的東西都是連在一起的,所以當(dāng)數(shù)據(jù)發(fā)生變化時(shí),用戶界面會(huì)自動(dòng)更新。
缺點(diǎn)是,LiveData并沒有像Rx那樣提供一個(gè)用于組合數(shù)據(jù)流或管理線程的工具包。
如果在一個(gè)典型的應(yīng)用程序的每一層中使用LiveData,看起來就像這樣。
為了在組件之間傳遞數(shù)據(jù),我們需要一種方法來映射和組合數(shù)據(jù)。MediatorLiveData就是LiveData提供的用于組合數(shù)據(jù)的工具,同時(shí)與Transformations類也提供了一些變換工具。
-
Transformations.map -
Transformations.switchMap
請注意,當(dāng)你的View被銷毀時(shí),你不需要銷毀這些訂閱,因?yàn)閂iew的lifecycle會(huì)被傳播到下游后繼續(xù)訂閱。
Patterns
One-to-one static transformation — map
在我們上面的例子中,ViewModel只是將數(shù)據(jù)從資源庫轉(zhuǎn)發(fā)到視圖,將其轉(zhuǎn)換為UI模型。每當(dāng)資源庫有新的數(shù)據(jù)時(shí),ViewModel只需對其進(jìn)行映射即可。
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
convertDataToMainUIModel(data)
}
}
這種轉(zhuǎn)變是非常簡單的。然而,如果上面的User數(shù)據(jù)是可以改變的,那么你需要使用switchMap。
One-to-one dynamic transformation — switchMap
考慮一下這個(gè)例子:你正在觀察一個(gè)暴露了User的用戶管理器,你需要獲取他們的ID,然后才能對存儲庫進(jìn)行觀察。
你不能在ViewModel的初始化中創(chuàng)建它們,因?yàn)橛脩鬒D不是立即可用的。你可以用switchMap來實(shí)現(xiàn)這一點(diǎn)。
class MainViewModel {
// val userId: LiveData<String> = ...
val repositoryResult = Transformations.switchMap(userManager.userID) { userID ->
repository.getDataForUser(userID)
}
}
switchMap內(nèi)部使用的也是MediatorLiveData,所以熟悉它很重要,隱藏,當(dāng)你想結(jié)合多個(gè)LiveData的來源時(shí),你需要使用它。
One-to-many dependency — MediatorLiveData
MediatorLiveData允許你將一個(gè)或多個(gè)數(shù)據(jù)源添加到一個(gè)LiveData觀察器中。
val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...
val result = MediatorLiveData<Int>()
result.addSource(liveData1) { value ->
result.setValue(value)
}
result.addSource(liveData2) { value ->
result.setValue(value)
}
這個(gè)例子來自官方文檔,當(dāng)任何一個(gè)數(shù)據(jù)來源發(fā)生變化時(shí),都會(huì)更新結(jié)果。請注意,數(shù)據(jù)不是自動(dòng)為你組合的,MediatorLiveData只是負(fù)責(zé)通知的工作。
為了在我們的示例應(yīng)用程序中實(shí)現(xiàn)轉(zhuǎn)換,我們需要將兩個(gè)不同的LiveDatas合并成一個(gè)。
使用MediatorLiveData來組合數(shù)據(jù)的方法是在不同的方法中添加來源和設(shè)置值。
fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
val liveData2 = userCheckinsDataSource.getCheckins(newUser)
val result = MediatorLiveData<UserDataResult>()
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
return result
}
數(shù)據(jù)的實(shí)際組合是在combineLatestData方法中完成的。
private fun combineLatestData(
onlineTimeResult: LiveData<Long>,
checkinsResult: LiveData<CheckinsResult>
): UserDataResult {
val onlineTime = onlineTimeResult.value
val checkins = checkinsResult.value
// Don't send a success until we have both results
if (onlineTime == null || checkins == null) {
return UserDataLoading()
}
// TODO: Check for errors and return UserDataError if any.
return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}
它檢查值是否準(zhǔn)備好或正確,并發(fā)出一個(gè)結(jié)果(加載、錯(cuò)誤或成功)。
When not to use LiveData
即使你想嘗試"反應(yīng)式",你也需要在到處添加LiveData之前了解其優(yōu)勢。如果你的應(yīng)用程序的某個(gè)組件與用戶界面沒有任何聯(lián)系,它可能不需要LiveData。
例如,你應(yīng)用中的一個(gè)用戶管理器會(huì)監(jiān)聽你的認(rèn)證提供者(如Firebase Auth)的變化,并向你的服務(wù)器上傳一個(gè)唯一的令牌。
令牌上傳者可以觀察用戶管理器,但用誰的生命周期?這個(gè)操作與View完全沒有關(guān)系。此外,如果View被銷毀,用戶令牌可能永遠(yuǎn)不會(huì)被上傳。
另一個(gè)選擇是使用令牌上傳器的observeForever(),并以某種方式鉤住用戶管理器的生命周期,在完成后刪除訂閱。
然而,你不需要讓所有的東西都能被觀察到。這個(gè)場景下,你可以讓用戶管理器直接調(diào)用令牌上傳器(或任何對你的架構(gòu)有意義的東西)。
?如果你的應(yīng)用程序的一部分不影響用戶界面,你可能不需要LiveData。
?
Antipattern: Sharing instances of LiveData
當(dāng)一個(gè)類將一個(gè)LiveData暴露給其他類時(shí),請仔細(xì)考慮是否要暴露同一個(gè)LiveData實(shí)例或不同的實(shí)例。
class SharedLiveDataSource(val dataSource: MyDataSource) {
// Caution: this LiveData is shared across consumers
private val result = MutableLiveData<Long>()
fun loadDataForUser(userId: String): LiveData<Long> {
result.value = dataSource.getOnlineTime(userId)
return result
}
}
如果這個(gè)類在你的應(yīng)用程序中是一個(gè)單例(只有一個(gè)實(shí)例),你就可以總是返回同一個(gè)LiveData,對嗎?不一定:這個(gè)類可能有多個(gè)消費(fèi)者。例如,考慮這個(gè)場景。
sharedLiveDataSource.loadDataForUser("1").observe(this, Observer {
// Show result on screen
})
而第二個(gè)消費(fèi)者也在使用它。
sharedLiveDataSource.loadDataForUser("2").observe(this, Observer {
// Show result on screen
})
第一個(gè)消費(fèi)者將收到屬于用戶 "2 "的數(shù)據(jù)的更新。
即使你認(rèn)為你只是從一個(gè)消費(fèi)者那里使用這個(gè)類,你也可能因?yàn)槭褂眠@種模式而最終出現(xiàn)錯(cuò)誤。例如,當(dāng)從一個(gè)Activity的一個(gè)實(shí)例導(dǎo)航到另一個(gè)實(shí)例時(shí),新的實(shí)例可能會(huì)暫時(shí)收到來自前一個(gè)實(shí)例的數(shù)據(jù)。請記住,LiveData會(huì)將最新的值分派給新的觀察者。另外,Lollipop中引入了Activity轉(zhuǎn)換,它們帶來了一個(gè)有趣的邊緣情況:兩個(gè)Activity處于活動(dòng)狀態(tài)。這意味著LiveData的唯一消費(fèi)者可能有兩個(gè)實(shí)例,其中一個(gè)可能會(huì)顯示錯(cuò)誤的數(shù)據(jù)。
解決這個(gè)問題的方法是為每個(gè)消費(fèi)者返回一個(gè)新的LiveData。
class SharedLiveDataSource(val dataSource: MyDataSource) {
fun loadDataForUser(userId: String): LiveData<Long> {
val result = MutableLiveData<Long>()
result.value = dataSource.getOnlineTime(userId)
return result
}
}
如果你要在消費(fèi)者之間共享一個(gè)LiveData實(shí)例之前,請仔細(xì)考慮。
MediatorLiveData smell: adding sources outside initialization
使用觀察者模式比持有對視圖的引用更安全(通常在MVP架構(gòu)中你會(huì)這樣做)。然而,這并不意味著你可以忘記泄漏的問題!
考慮一下這個(gè)數(shù)據(jù)源。
class SlowRandomNumberGenerator {
private val rnd = Random()
fun getNumber(): LiveData<Int> {
val result = MutableLiveData<Int>()
// Send a random number after a while
Executors.newSingleThreadExecutor().execute {
Thread.sleep(500)
result.postValue(rnd.nextInt(1000))
}
return result
}
}
它只是在500ms后返回一個(gè)帶有隨機(jī)值的新LiveData。這并沒有什么問題。
在ViewModel中,我們需要公開一個(gè)randomNumber屬性,從生成器中獲取數(shù)字。為此使用MediatorLiveData并不理想,因?yàn)樗竽阍诿看涡枰聰?shù)字時(shí)都要添加源。
val randomNumber = MediatorLiveData<Int>()
/**
* *Don't do this.*
*
* Called when the user clicks on a button
*
* This function adds a new source to the result but it doesn't remove the previous ones.
*/
fun onGetNumber() {
randomNumber.addSource(numberGenerator.getNumber()) {
randomNumber.value = it
}
}
如果每次用戶點(diǎn)擊按鈕時(shí),我們都向MediatorLiveData添加一個(gè)源,那么該應(yīng)用就能按預(yù)期工作。然而,我們正在泄露所有以前的LiveDatas,這些LiveDatas不會(huì)再發(fā)送更新,所以這是一種浪費(fèi)。
你可以存儲一個(gè)對源的引用,然后在添加新的源之前將其刪除。(Spoiler: this is what Transformations.switchMap does! See solution below.)
我們不要使用MediatorLiveData,而是嘗試(但失敗了)用Transformation.map來解決這個(gè)問題。
Transformation smell: Transformations outside initialization
使用前面的例子,這就不可行了。
var lateinit randomNumber: LiveData<Int>
/**
* Called on button click.
*/
fun onGetNumber() {
randomNumber = Transformations.map(numberGenerator.getNumber()) {
it
}
}
這里有一個(gè)重要的問題需要理解。變換在調(diào)用時(shí)創(chuàng)建一個(gè)新的LiveData(包括map和switchMap)。在這個(gè)例子中,隨機(jī)數(shù)(randomNumber)被暴露在視圖中,但每次用戶點(diǎn)擊按鈕時(shí)它都會(huì)被重新分配。觀察者只在訂閱的時(shí)候接收分配給var的LiveData的更新,這是非常常見的。
viewmodel.randomNumber.observe(this, Observer { number ->
numberTv.text = resources.getString(R.string.random_text, number)
})
這個(gè)訂閱發(fā)生在onCreate()中,所以如果之后viewmodel.randomNumber LiveData實(shí)例發(fā)生變化,觀察者將不會(huì)被再次調(diào)用。
換句話說。不要在var中使用Livedata。在初始化的時(shí)候,要將轉(zhuǎn)換的內(nèi)容寫入。
Solution: wire transformations during initialization
將暴露的LiveData初始化為一個(gè)transformation。
private val newNumberEvent = MutableLiveData<Event<Any>>()
val randomNumber: LiveData<Int> = Transformations.switchMap(newNumberEvent) {
numberGenerator.getNumber()
}
在LiveData中使用一個(gè)事件來指示何時(shí)請求一個(gè)新號碼。
/**
* Notifies the event LiveData of a new request for a random number.
*/
fun onGetNumber() {
newNumberEvent.value = Event(Unit)
}
如果你不熟悉這種模式,請看這篇關(guān)于Activity的文章。
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
Bonus section
Tidying up with Kotlin
上面的MediatorLiveData例子顯示了一些代碼的重復(fù),所以我們可以利用Kotlin的擴(kuò)展函數(shù)。
/**
* Sets the value to the result of a function that is called when both `LiveData`s have data
* or when they receive updates after that.
*/
fun <T, A, B> LiveData<A>.combineAndCompute(other: LiveData<B>, onChange: (A, B) -> T): MediatorLiveData<T> {
var source1emitted = false
var source2emitted = false
val result = MediatorLiveData<T>()
val mergeF = {
val source1Value = this.value
val source2Value = other.value
if (source1emitted && source2emitted) {
result.value = onChange.invoke(source1Value!!, source2Value!! )
}
}
result.addSource(this) { source1emitted = true; mergeF.invoke() }
result.addSource(other) { source2emitted = true; mergeF.invoke() }
return result
}
存儲庫現(xiàn)在看起來干凈多了。
fun getDataForUser(newUser: String?): LiveData<UserDataResult> {
if (newUser == null) {
return MutableLiveData<UserDataResult>().apply { value = null }
}
return userOnlineDataSource.getOnlineTime(newUser)
.combineAndCompute(userCheckinsDataSource.getCheckins(newUser)) { a, b ->
UserDataSuccess(a, b)
}
}
LiveData and RxJava
最后,讓我們來討論一個(gè)顯而易見而又沒人愿意討論的問題。LiveData被設(shè)計(jì)為允許視圖觀察ViewModel。一定要把它用在這上面! 即使你已經(jīng)使用了Rx,你也可以用LiveDataReactiveStreams進(jìn)行通信。
如果你想在表現(xiàn)層之外使用LiveData,你可能會(huì)發(fā)現(xiàn)MediatorLiveData并沒有像RxJava那樣提供一個(gè)工具包來組合和操作數(shù)據(jù)流。然而,Rx有一個(gè)陡峭的學(xué)習(xí)曲線。LiveData轉(zhuǎn)換(和Kotlin魔法)的組合可能足以滿足你的情況,但如果你(和你的團(tuán)隊(duì))已經(jīng)投資學(xué)習(xí)RxJava,你可能不需要LiveData。
如果你使用auto-dispose,那么為此使用LiveData將是多余的。
原文鏈接:https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 專注 Android-Kotlin-Flutter 歡迎大家訪問
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點(diǎn)擊原文一鍵直達(dá)
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點(diǎn)個(gè)“三連”支持一下??
