【譯】LiveData 在 SnackBar/Navigation 情景下的使用(SingleLiveEvent)
前言
本文翻譯自【 ofollow,noindex">LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) 】,詳細介紹了 liveData 的使用。感謝作者 Jose Alcérreca 。水平有限,歡迎指正討論。
前面兩篇介紹 LiveData 的文章( Android-AAC-Architecture-Communication-between-ViewModel-and-View/" target="_blank" rel="nofollow,noindex">【譯】Android Architecture - ViewModel 與 View 的通訊 和 【譯】LiveData 使用詳解 )都提到了 SingleLiveEvent
,本篇重點來看下它是個什麼東西,以及它的使用場景。
正文
LiveData
一般被用於 View
與 ViewModel
的通訊。View 通過訂閱 LiveData 的變化來更新 UI,這適用於需要長時間展示在螢幕上的資料。

1-LiveData-Continuous-View.png
然而,有些資料可能只需要展示一次,例如 SnackBar
訊息,一個 Navigation
事件,或者一個觸發 Dialog 展示/消失的資料。

2-LiveData-Once-View.png
我們不應該嘗試用 Architecture Components 基礎或擴充套件庫來解決這個問題,相反這是一個設計問題。我們建議你將這些事件作為資料狀態的一部分。在本文中,我們將展示一些常見錯誤和推薦方法。
:x: Bad: 1. Using LiveData for events
這種用法是在 LiveData 中儲存一個 SnackBar
訊息,或一個 Navigation
事件。儘管原則上是 LiveData 的正常使用,但這存在一些問題。
在一個包含首頁和詳情頁的應用中,首頁的 ListViewModel.kt 程式碼如下:
// Don't use this for events class ListViewModel : ViewModel { private val _navigateToDetails = MutableLiveData<Boolean>() val navigateToDetails : LiveData<Boolean> get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.value = true } }
MyFragment.kt 程式碼如下:
myViewModel.navigateToDetails.observe(this, Observer { if (it) startActivity(DetailsActivity...) })
這種使用方式的問題是: _navigateToDetails
中的值會永遠為 true
,從而導致無法回到首頁。
復現步驟是:
- 使用者點選按鈕,啟動詳情頁
DetailsActivity
- 使用者點選返回鍵,返回到主介面
MasterActivity
- 這時
MasterActivity
由非活動狀態恢復到活動狀態 - 但
myViewModel
觀察到_navigateToDetails
仍舊為true
,就又跳轉到詳情頁DetailsActivity
一種看起來沒問題的解決方案是:頁面跳轉後立馬把標誌位設為 false
,如 ListViewModel.kt 所示:
fun userClicksOnButton() { _navigateToDetails.value = true _navigateToDetails.value = false // Don't do this }
然而,需要注意的是: LiveData 不能保證發射它接收到的每個資料值 。例如我們在沒有活動的觀察者時設定了一個新值,這個新值不會被髮送,此外,在多個子執行緒中操作 LiveData 可能發生競爭狀況,從而導致觀察者只會收到一次回撥。
但這個方案的主要問題是: 別人很難看懂這個程式碼,並且這種程式碼也很醜陋 。那麼,我們應該怎麼確保在導航事件發生後恢復初值呢?
:x: Better: 2. Using LiveData for events, resetting event values in observer
另一種稍微好點,但仍有問題的方案是:View 告訴 ViewModel,導航事件已經完成,LiveData 應該恢復預設值了。
Usage
基於第一節的例子,對觀察者程式碼做如下改動即可, MyFragment.kt :
listViewModel.navigateToDetails.observe(this, Observer { if (it) { myViewModel.navigateToDetailsHandled() startActivity(DetailsActivity...) } })
然後在 ListViewModel.kt 中新增一個 navigateToDetailsHandled()
方法:
class ListViewModel : ViewModel { private val _navigateToDetails = MutableLiveData<Boolean>() val navigateToDetails : LiveData<Boolean> get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.value = true } fun navigateToDetailsHandled() { _navigateToDetails.value = false } }
Issues
這種方法的問題是:存在很多樣板程式碼,ViewModel 中每新增一個事件都要新增一個對應的方法,並且很容易出錯。此外,觀察者(View)很容易忘記呼叫 ViewModel 的這個方法。
:white_check_mark: OK: Use SingleLiveEvent
一種還可以接受的解決方案是: SingleLiveEvent 。這個類是 Google 官方 Demo 中的適用於這種特殊場景的解決方案,它是一個僅傳送一次更新的 LiveData。
public class SingleLiveEvent<T> extends MutableLiveData<T> { private static final String TAG = "SingleLiveEvent"; private final AtomicBoolean mPending = new AtomicBoolean(false); @MainThread public void observe(LifecycleOwner owner, final Observer<T> observer) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); } // Observe the internal MutableLiveData super.observe(owner, new Observer<T>() { @Override public void onChanged(@Nullable T t) { if (mPending.compareAndSet(true, false)) { observer.onChanged(t); } } }); } @MainThread public void setValue(@Nullable T t) { mPending.set(true); super.setValue(t); } /** * Used for cases where T is Void, to make calls cleaner. */ @MainThread public void call() { setValue(null); } }
Usage
ListViewModel.kt 程式碼如下:
class ListViewModel : ViewModel { private val _navigateToDetails = SingleLiveEvent<Any>() val navigateToDetails : LiveData<Any> get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.call() } }
MyFragment.kt 程式碼如下:
myViewModel.navigateToDetails.observe(this, Observer { startActivity(DetailsActivity...) })
Issues
SingleLiveEvent
的問題在於:它僅限於一個觀察者。如果你無意中添加了多個,則只會有一個收到回撥,並且無法保證哪一個會收到。

3-LiveData-SingleLiveEvent-Issue.png
:white_check_mark: Recommended: Use an Event wrapper
推薦的解決方案是:封裝事件。通過這種方式,我們可以明確地管理實踐是否被處理,從而減少錯誤。
Usage
Event.kt 封裝了事件,程式碼如下:
/** * 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 }
ListViewModel.kt 程式碼如下:
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 } }
MyFragment.kt 程式碼如下:
myViewModel.navigateToDetails.observe(this, Observer { // Only proceed if the event has never been handled it.getContentIfNotHandled()?.let { startActivity(DetailsActivity...) } })
這種方案的優勢在於:使用者需要呼叫 Event#getContentIfNotHandled()
方法或 Event#peekContent()
來指定跳轉 Intent
。這種方案將事件作為 UI 狀態的一部分:現在它們只是一個已被消費或未被消費的訊息。

4With an Event wrapper, you can add multiple observers to a single-use event
總結
design events as part of your state.我們可以包裝自己的 Event 來滿足自己的需求。
Bonus! 如果有很多事件,可以使用 EventObserver 避免一些樣板程式碼。
參考
聯絡
我是 xiaobailong24 ,您可以通過以下平臺找到我: