檢視(Activity或Fragment)與ViewModel進行通訊的一種便捷的方式是使用LiveData,檢視可以訂閱LiveData中的資料變化並對其作出反饋。這適用於那些需要一直在螢幕上顯示的資料。
但是,有些資料只應該被消費一次,比如顯示Snackbar訊息、導航事件或者彈出對話方塊。
不要試圖使用第三方庫或者是擴充套件Architecture來解決這個問題,你應當把它看作是一個設計問題。We recommend you treat your events as part of your state。本文將列出一些解決這個問題的錯誤方法,並給出我們推薦的方法。
❌ Bad: 1. Using LiveData for events
這種方法在LiveData物件內部直接持有Snackbar訊息或導航訊號。原則上它看起來像一個普通的LiveData物件,但是它存在一些問題。
在一個列表/詳情的應用中,列表介面的ViewModel如下:
// Do not use this for events
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
複製程式碼
在檢視(Activity或Fragment)裡:
listViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
複製程式碼
這種方法的問題在於,_navigateToDetails的值會一直為true,導致無法返回到列表介面。詳細情況如下:
- 使用者在列表介面中點選按鈕進入詳情介面。
- 使用者按下返回鍵返回到列表介面。
- 列表介面的觀察者觀察到_navigateToDetails的值為
true
,會再次錯誤的跳轉到詳情介面。
一種解決方法是在_navigateToDetails的值被設定為true
之後,立即修改它的值為flase
:
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don`t do this
}
複製程式碼
上面的方法是錯誤的。因為LiveData中雖然可以儲存資料,但不保證發出它接收到的每個值。例如:在沒有觀察者處於活動狀態時設定一個值,這時候如果再給它設定一個新的值,那麼新的值將直接替換舊的值。此外,在不同執行緒中設定值可能會導致衝突,使得它只會向觀察者發出一次變化通知。
但是這種方法的主要問題在於如何確保導航事件發生後LiveData中的值一定會被被重置?
❌ Better: 2. Using LiveData for events, resetting event values in observer
上面的方法還可以衍生出另一種解決方法:在檢視中告訴ViewModel你已經處理了該導航事件,並且希望它重置該事件,即修改_navigateToDetails的值為flase
。
只需對方法1的程式碼做簡單的修改:
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})
複製程式碼
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中的這些方法。
✔️ OK: Use SingleLiveEvent
SingleLiveEvent類就是為解決上述問題而建立的,它是一個只會傳送一次資料更新的LiveData。
用法
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
複製程式碼
listViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
複製程式碼
SingleLiveEvent的問題在於它僅限於一個觀察者。如果您無意中新增了多個觀察者,那麼只會有一個觀察者會對它作出反饋,並且不能保證是哪一個。
✔️ Recommended: Use an Event wrapper
使用這種方法您能明確地知道事件是否已被處理,從而減少錯誤。
用法
/**
* 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
}
}
複製程式碼
listViewModell.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
複製程式碼
這種方法的好處是使用者可以通過呼叫getContentIfNotHandled()方法來將導航事件與目標觀察者關聯起來。它把導航事件視為一種狀態:consumed 或not consumed。
總而言之:你應當把事件設計為一種狀態。你可以自定義一個事件包裝器以滿足您的需求。
如果你的應用中有許多類似的事件,建議使用EventObserver類來刪除一些重複性程式碼。