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

Android_開發者發表於2019-03-04

檢視層(Activity 或者 Fragment)與 ViewModel 層進行通訊的一種便捷的方式就是使用 LiveData 來進行觀察。這個檢視層訂閱 Livedata 的資料變化並對其變化做出反應。這適用於連續不斷顯示在螢幕的資料。

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

但是,有一些資料只會消費一次,就像是 Snackbar 訊息,導航事件或者對話方塊。

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

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

❌ 錯誤: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 中的值會長時間保持為真,並且無法返回到第一個螢幕。一步一步進行分析:

  1. 使用者點選按鈕 Details Activity 啟動。
  2. 使用者使用者按下返回,回到主 Activity。
  3. 觀察者在 Activity 處於回退棧時從非監聽狀態再次變成監聽狀態。
  4. 但是該值仍然為 “真”,因此 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 的問題在於它僅限於一個觀察者。如果您無意中新增了多個,則只會呼叫一個,並且不能保證哪一個。

[譯] 在 SnackBar,Navigation 和其他事件中使用 LiveData(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() 來指定意圖。這個方法將事件建模為狀態的一部分:他們現在只是一個消耗或者不消耗的訊息。

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

使用事件包裝器,您可以將多個觀察者新增到一次性事件中。


總之:把事件設計成你的狀態的一部分。使用您自己的事件包裝器並根據您的需求進行定製。

銀彈!若您最終發生大量事件,請使用這個 EventObserver 可以刪除很多無用的程式碼。

感謝 Don TurnerNick Butcher,和 Chris Banes

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


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

相關文章