- 原文地址:LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)
- 原文做者:Jose Alcérreca
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:wzasd
- 校對者:LeeSniper
視圖層(Activity 或者 Fragment)與 ViewModel 層進行通信的一種便捷的方式就是使用 LiveData
來進行觀察。這個視圖層訂閱 Livedata 的數據變化並對其變化作出反應。這適用於接二連三顯示在屏幕的數據。前端
可是,有一些數據只會消費一次,就像是 Snackbar 消息,導航事件或者對話框。java
這應該被視爲設計問題,而不是試圖經過架構組件的庫或者擴展來解決這個問題。咱們建議您將您的事件視爲您的狀態的一部分。在本文中,咱們將展現一些常見的錯誤方法,以及推薦的方式。android
這種方法來直接的在 LiveData 對象的內部持有 Snackbar 消息或者導航信息。儘管原則上看起來像是普通的 LiveData 對象能夠用在這裏,可是會出現一些問題。ios
在一個主/從應用程序中,這裏是主 ViewModel:git
// 不要使用這個事件
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
複製代碼
在視圖層(Activity 或者 Fragment):github
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
複製代碼
這種方法的問題是 _navigateToDetails
中的值會長時間保持爲真,而且沒法返回到第一個屏幕。一步一步進行分析:後端
解決方法是從 ViewModel 中將導航的標誌點擊後馬上設爲 false;bash
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this } 複製代碼
可是,須要記住的一件很重要的事就是 LiveData 儲存這個值,可是不保證發出它接受到的每一個值。例如:當沒有觀察者處於監聽狀態時,能夠設置一個值,所以新的值將會替換它。此外,從不一樣線程設置值的時候可能會致使資源競爭,只會向觀察者發出一次改變信號。架構
可是這種方法的主要問題是難以理解和不簡潔。在導航事件發生後,咱們如何確保值被重置呢?app
經過這種方法,您能夠添加一種方法來從視圖中支出您已經處理了該事件,而且重置該事件。
對咱們的觀察者進行一些小改動,咱們就有了這樣的解決方案:
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})
複製代碼
像下面這樣在 ViewModel 中添加新的方法:
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
fun navigateToDetailsHandled() {
_navigateToDetails.value = false
}
}
複製代碼
這種方法的問題是有一些死板(每一個事件在 ViewModel 中有一個新的方法),而且很容易出錯,觀察者很容易忘記調用這個 ViewModel 的方法。
這個 SingleLiveEvent 類是爲了適用於特定場景的解決方法。這是一個只會發送一次更新的 LiveData。
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
複製代碼
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
複製代碼
SingleLiveEvent 的問題在於它僅限於一個觀察者。若是您無心中添加了多個,則只會調用一個,而且不能保證哪個。
在這種方法中,您能夠明確地管理事件是否已經被處理,從而減小錯誤。
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled. */ fun peekContent(): T = content } 複製代碼
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
複製代碼
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
複製代碼
這種方法的優勢在於用戶使用 getContentIfNotHandled()
或者 peekContent()
來指定意圖。這個方法將事件建模爲狀態的一部分:他們如今只是一個消耗或者不消耗的消息。
使用事件包裝器,您能夠將多個觀察者添加到一次性事件中。
總之:把事件設計成你的狀態的一部分。使用您本身的事件包裝器並根據您的需求進行定製。
銀彈!若您最終發生大量事件,請使用這個 EventObserver 能夠刪除不少無用的代碼。
感謝 Don Turner,Nick Butcher,和 Chris Banes。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。