[譯] 在 SnackBar,Navigation 和其餘事件中使用 LiveData(SingleLiveEvent 案例)

視圖層(Activity 或者 Fragment)與 ViewModel 層進行通信的一種便捷的方式就是使用 LiveData 來進行觀察。這個視圖層訂閱 Livedata 的數據變化並對其變化作出反應。這適用於接二連三顯示在屏幕的數據。前端

可是,有一些數據只會消費一次,就像是 Snackbar 消息,導航事件或者對話框。java

這應該被視爲設計問題,而不是試圖經過架構組件的庫或者擴展來解決這個問題。咱們建議您將您的事件視爲您的狀態的一部分。在本文中,咱們將展現一些常見的錯誤方法,以及推薦的方式。android

❌ 錯誤:1. 使用 LiveData 來解決事件

這種方法來直接的在 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 中的值會長時間保持爲真,而且沒法返回到第一個屏幕。一步一步進行分析:後端

  1. 用戶點擊按鈕 Details Activity 啓動。
  2. 用戶用戶按下返回,回到主 Activity。
  3. 觀察者在 Activity 處於回退棧時從非監聽狀態再次變成監聽狀態。
  4. 可是該值仍然爲 「真」,所以 Detail Activity 啓動出錯。

解決方法是從 ViewModel 中將導航的標誌點擊後馬上設爲 false;bash

fun userClicksOnButton() {
    _navigateToDetails.value = true
    _navigateToDetails.value = false // Don't do this } 複製代碼

可是,須要記住的一件很重要的事就是 LiveData 儲存這個值,可是不保證發出它接受到的每一個值。例如:當沒有觀察者處於監聽狀態時,能夠設置一個值,所以新的值將會替換它。此外,從不一樣線程設置值的時候可能會致使資源競爭,只會向觀察者發出一次改變信號。架構

可是這種方法的主要問題是難以理解和不簡潔。在導航事件發生後,咱們如何確保值被重置呢?app

❌ 可能更好一些: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 TurnerNick Butcher,和 Chris Banes

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索