- 原文地址: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 訊息,導航事件或者對話方塊。
這應該被視為設計問題,而不是試圖通過架構元件的庫或者擴充套件來解決這個問題。我們建議您將您的事件視為您的狀態的一部分。在本文中,我們將展示一些常見的錯誤方法,以及推薦的方式。
❌ 錯誤:1. 使用 LiveData 來解決事件
這種方法來直接的在 LiveData 物件的內部持有 Snackbar 訊息或者導航資訊。儘管原則上看起來像是普通的 LiveData 物件可以用在這裡,但是會出現一些問題。
在一個主/從應用程式中,這裡是主 ViewModel:
// 不要使用這個事件
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
複製程式碼
在檢視層(Activity 或者 Fragment):
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
複製程式碼
這種方法的問題是 _navigateToDetails
中的值會長時間保持為真,並且無法返回到第一個螢幕。一步一步進行分析:
- 使用者點選按鈕 Details Activity 啟動。
- 使用者使用者按下返回,回到主 Activity。
- 觀察者在 Activity 處於回退棧時從非監聽狀態再次變成監聽狀態。
- 但是該值仍然為 “真”,因此 Detail Activity 啟動出錯。
解決方法是從 ViewModel 中將導航的標誌點選後立刻設為 false;
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this
}
複製程式碼
但是,需要記住的一件很重要的事就是 LiveData 儲存這個值,但是不保證發出它接受到的每個值。例如:當沒有觀察者處於監聽狀態時,可以設定一個值,因此新的值將會替換它。此外,從不同執行緒設定值的時候可能會導致資源競爭,只會向觀察者發出一次改變訊號。
但是這種方法的主要問題是難以理解和不簡潔。在導航事件發生後,我們如何確保值被重置呢?
❌ 可能更好一些:2. 使用 LiveData 進行事件處理,在觀察者中重置事件的初始值
通過這種方法,您可以新增一種方法來從檢視中支出您已經處理了該事件,並且重置該事件。
用法
對我們的觀察者進行一些小改動,我們就有了這樣的解決方案:
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
這個 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。