一篇掌握LiveData transformations

點(diǎn)擊上方藍(lán)字關(guān)注我,知識(shí)會(huì)給你力量

這個(gè)系列我做了協(xié)程和Flow開(kāi)發(fā)者的一系列文章的翻譯,旨在了解當(dāng)前協(xié)程、Flow、LiveData這樣設(shè)計(jì)的原因,從設(shè)計(jì)者的角度,發(fā)現(xiàn)他們的問(wèn)題,以及如何解決這些問(wèn)題,pls enjoy it。
在使用Android架構(gòu)組件時(shí),LiveData是一個(gè)很好的工具。在我知道如何使用Transformations類之前,我一直在濫用LiveData,并產(chǎn)生了大量的爛代碼。在使用LiveData和架構(gòu)組件的幾年中,我想我已經(jīng)找到了一些好的做法和模式,我想與你分享。
The basics…
對(duì)LiveData進(jìn)行轉(zhuǎn)換是非常容易的,有一個(gè)名為T(mén)ransformations的輔助類正是為了這個(gè)目的。這個(gè)類提供了三個(gè)靜態(tài)方法:map、switchMap和distinctUntilChanged,這些方法將在下面解釋。下面的所有例子都將使用下面的數(shù)據(jù)類,它代表了我們從數(shù)據(jù)庫(kù)或后臺(tái)API接收的一個(gè)Player數(shù)據(jù)。這個(gè)Player模型只有一個(gè)名字和分?jǐn)?shù)字段,以方便舉例,但在現(xiàn)實(shí)中,它將有更多的字段。
data class Player(val name: String, val score: Int = 0, val ...)
map
將LiveDatain的值轉(zhuǎn)換為另一個(gè)值。下面是一個(gè)簡(jiǎn)單的例子,說(shuō)明如何使用它。
val player: LiveData<Player> = ...
val playerName: LiveData<String> =
Transformations.map(player) { it.name }
switchMap
將一個(gè)LiveDatain的值轉(zhuǎn)換為另一個(gè)LiveData。switchMap的轉(zhuǎn)換可能有點(diǎn)棘手,所以讓我們從一個(gè)簡(jiǎn)單的例子開(kāi)始。我們想為Player實(shí)現(xiàn)一個(gè)基本的搜索功能。每次搜索文本發(fā)生變化時(shí),我們都想更新搜索結(jié)果。下面的代碼顯示了它是如何工作的。
val searchQuery: LiveData<String> = ...
fun getSearchResults(query: String): LiveData<List<Player>> = ...
val searchResults: LiveData<List<Player>> =
Transformations.switchMap(searchQuery) { getSearchResults(it) }
distinctUntilChanged
對(duì)LiveData進(jìn)行過(guò)濾,除非數(shù)值發(fā)生了變化,否則不會(huì)被檢索出來(lái)。很多時(shí)候,我們可能會(huì)收到一個(gè)不包含任何相關(guān)變化的通知。如果我們監(jiān)聽(tīng)的是所有球員的名字,我們不想在分?jǐn)?shù)發(fā)生變化時(shí)更新用戶界面。這就是distinctUntilChanged方法的用處。
val players: LiveData<List<Player>> = ...
val playerNames: LiveData<List<String>> =
Transformations.distinctUntilChanged(
Transformations.map(players) { players -> players.map { it.name } }
)
這是一個(gè)非常好的功能,我在我的代碼中經(jīng)常使用它。對(duì)于我的使用情況,它主要與RecyclerView/適配器的更新有關(guān)。
livedata-ktx extensions for Transformations
上述所有的Transformations類函數(shù)也可以作為L(zhǎng)iveData的擴(kuò)展函數(shù),使用下面的依賴。
androidx.lifecycle:lifecycle-livedata-ktx:<version>
有了它,例如,你可以把上面的例子改寫(xiě)成下面這樣。
val players: LiveData<List<Player>> = ...
val playerNames: LiveData<List<String>> = players.map { it.map { player -> player.name } }
.distinctUntilChanged()
Behind the scenes of the Transformations class
我們剛剛涵蓋了3個(gè)簡(jiǎn)單的轉(zhuǎn)換,你實(shí)際上可以自己寫(xiě)。所有這些都是使用MediatorLiveData類編寫(xiě)的。MediatorLiveData類是我在處理LiveData時(shí)使用最多的類(盡管我在有意義的時(shí)候使用map / switchMap / distinctUntilChanged)。
為了給你一個(gè)例子,說(shuō)明你什么時(shí)候應(yīng)該創(chuàng)建你自己的MediatorLiveData類,看看這段代碼。
val players: LiveData<List<Player>> = ...
val dbGame: LiveData<GameEntity> = ...
val game: LiveData<Game> =
Transformations.map(dbGame) { game ->
val players = this.players.value // Getting current players here may be unsafe
Game(players = game.playerIds.mapNotNull { playerId ->
players?.find { it.id == playerId }
})
}
通過(guò)只映射dbGame的變化,我在Player更新時(shí)取了玩家的當(dāng)前值(this.player.value)。所以,當(dāng)Player被更新時(shí),我并沒(méi)有更新Game。為了解決這個(gè)問(wèn)題,我應(yīng)該使用MediatorLiveData來(lái)合并Player和Game,如果他們中的任何一個(gè)被更新。這將看起來(lái)像這樣。
val players: LiveData<List<Player>> = ...
val dbGame: LiveData<GameEntity> = ...
val game: LiveData<Game> = MediatorLiveData<Game>()
.apply {
fun update() {
val players = players.value ?: return
val game = dbGame.value ?: return
value = Game(players = game.playerIds
.mapNotNull { playerId ->
players?.find { it.id == playerId }
}
)
}
addSource(players) { update() }
addSource(dbGame) { update() }
update()
}
有了這個(gè)解決方案,每當(dāng)球員或dbGame更新時(shí),我都會(huì)得到Game更新。
MediatorLiveData
MediatorLiveData可以轉(zhuǎn)換、過(guò)濾和合并其他LiveData實(shí)例。每當(dāng)我創(chuàng)建MediatorLiveData時(shí),我傾向于遵循同樣的模式,它看起來(lái)像這樣。
val a = MutableLiveData<Int>(40)
val b = MutableLiveData<Int>(2)
val sum: LiveData<Int> = MediatorLiveData<Int>().apply {
fun update() {
// OPTION 3
val aVal = a.value ?: return
val bVal = b.value ?: return
// OPTION 4
value = aVal + bVal
}
// OPTION 1
addSource(a) { update() }
addSource(b) { update() }
// OPTION 2
update()
}
在這個(gè)例子中,我正在觀察兩個(gè)LiveData源(a和b)。我在調(diào)解器創(chuàng)建時(shí)調(diào)用了更新函數(shù),只有在兩個(gè)源都是非空的情況下才會(huì)發(fā)出一個(gè)值。這種模式非常通用,但讓我們一個(gè)一個(gè)地走完每一步。
方案1
在從這個(gè)LiveData發(fā)出任何東西之前,你想監(jiān)控哪些源的變化。這可以只是一個(gè)單一的源(或更多),但沒(méi)有固定的上限。(即讓你對(duì)單個(gè)LiveData進(jìn)行條件映射或合并多個(gè)LiveDatas)
方案2
如果你想在創(chuàng)建MediatorLiveData時(shí)設(shè)置一個(gè)初始值,在這里調(diào)用內(nèi)部更新函數(shù)。為了簡(jiǎn)單起見(jiàn),我通常調(diào)用我的更新函數(shù),但只是設(shè)置MediatorLiveData的值/postValue也可以。在某些情況下,我不想發(fā)出一個(gè)初始值,因?yàn)槲蚁M赼或b還沒(méi)有設(shè)置的情況下發(fā)出空值。那么我就跳過(guò)在這里調(diào)用更新或設(shè)置初始值。
方案3
因?yàn)橹灰猘或b發(fā)出更新,就會(huì)調(diào)用update,我們必須期望a和b為空。有時(shí)你實(shí)際上想更新你的MediatorLiveData,即使一個(gè)或多個(gè)來(lái)源目前是空的,但這是一個(gè)很好的方法,在從MediatorLiveData發(fā)出新值之前,確保局部變量aVal和bVal不是空的。你甚至可以在這里應(yīng)用更多的驗(yàn)證/過(guò)濾,以減少你所創(chuàng)建的最終MediatorLiveData的排放。
方案4
由于MediatorLiveData是一個(gè)LiveData實(shí)例,我們可以設(shè)置值(像上面的例子)或調(diào)用postValue(如果由于某種原因,你在發(fā)射值時(shí)不在主線程上)。這也是你決定如何轉(zhuǎn)換源數(shù)據(jù)值的地方。上面的例子只是將aVal和bVal相加,但你當(dāng)然可以在這里應(yīng)用你想要的任何轉(zhuǎn)換。
結(jié)論
在所有的LiveData轉(zhuǎn)換中使用map、switchMap和distinctUntilChanged。除非有必要,否則應(yīng)避免編寫(xiě)自己的轉(zhuǎn)換,并嘗試結(jié)合操作來(lái)創(chuàng)建更復(fù)雜的轉(zhuǎn)換。
使用distinctUntilChanged來(lái)避免發(fā)出相同的數(shù)據(jù),這將導(dǎo)致不必要的UI更新。
如果你發(fā)現(xiàn)自己在地圖/switchMap內(nèi)或觀察塊內(nèi)使用.value屬性獲得另一個(gè)LiveData的當(dāng)前值,你應(yīng)該考慮創(chuàng)建一個(gè)MediatorLiveData來(lái)正確合并來(lái)源。
原文鏈接:https://proandroiddev.com/livedata-transformations-4f120ac046fc
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點(diǎn)擊原文一鍵直達(dá)
專注 Android-Kotlin-Flutter 歡迎大家訪問(wèn)
往期推薦
更文不易,點(diǎn)個(gè)“三連”支持一下??
