【譯】LiveData 在 SnackBar/Navigation 情景下的使用(SingleLiveEvent)

前言

本文翻譯自【LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)】,詳細介紹了 liveData 的使用。感謝做者 Jose Alcérreca。水平有限,歡迎指正討論。 前面兩篇介紹 LiveData 的文章(【譯】Android Architecture - ViewModel 與 View 的通訊【譯】LiveData 使用詳解)都提到了 SingleLiveEvent,本篇重點來看下它是個什麼東西,以及它的使用場景。java

正文

LiveData 通常被用於 ViewViewModel 的通訊。View 經過訂閱 LiveData 的變化來更新 UI,這適用於須要長時間展現在屏幕上的數據。 android

1-LiveData-Continuous-View.png

然而,有些數據可能只須要展現一次,例如 SnackBar 消息,一個 Navigation 事件,或者一個觸發 Dialog 展現/消失的數據。 git

2-LiveData-Once-View.png

咱們不該該嘗試用 Architecture Components 基礎或擴展庫來解決這個問題,相反這是一個設計問題。咱們建議你將這些事件做爲數據狀態的一部分。在本文中,咱們將展現一些常見錯誤和推薦方法。github

❌ Bad: 1. Using LiveData for events

這種用法是在 LiveData 中保存一個 SnackBar 消息,或一個 Navigation 事件。儘管原則上是 LiveData 的正常使用,但這存在一些問題。 在一個包含首頁和詳情頁的應用中,首頁的 ListViewModel.kt 代碼以下:c#

// 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 代碼以下:bash

myViewModel.navigateToDetails.observe(this, Observer {
    if (it) startActivity(DetailsActivity...)
})
複製代碼

這種使用方式的問題是:_navigateToDetails 中的值會永遠爲 true,從而致使沒法回到首頁。 復現步驟是:app

  1. 用戶點擊按鈕,啓動詳情頁 DetailsActivity
  2. 用戶點擊返回鍵,返回到主界面 MasterActivity
  3. 這時 MasterActivity 由非活動狀態恢復到活動狀態
  4. myViewModel 觀察到 _navigateToDetails 仍舊爲 true,就又跳轉到詳情頁 DetailsActivity

一種看起來沒問題的解決方案是:頁面跳轉後立馬把標誌位設爲 false,如 ListViewModel.kt 所示:mvvm

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

然而,須要注意的是:LiveData 不能保證發射它接收到的每一個數據值。例如咱們在沒有活動的觀察者時設置了一個新值,這個新值不會被髮送,此外,在多個子線程中操做 LiveData 可能發生競爭情況,從而致使觀察者只會收到一次回調。 但這個方案的主要問題是:別人很難看懂這個代碼,而且這種代碼也很醜陋。那麼,咱們應該怎麼確保在導航事件發生後恢復初值呢?ide

❌ Better: 2. Using LiveData for events, resetting event values in observer

另外一種稍微好點,但仍有問題的方案是:View 告訴 ViewModel,導航事件已經完成,LiveData 應該恢復默認值了。ui

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 的這個方法。

✅ 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

✅ 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,您能夠經過如下平臺找到我:

相關文章
相關標籤/搜索