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

前言

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

正文

LiveData 一般被用于 ViewViewModel 的通信。View 通過訂閱 LiveData 的變化來更新 UI,這適用于需要長(zhǎng)時(shí)間展示在屏幕上的數(shù)據(jù)。

1-LiveData-Continuous-View.png

然而,有些數(shù)據(jù)可能只需要展示一次,例如 SnackBar 消息,一個(gè) Navigation 事件,或者一個(gè)觸發(fā) Dialog 展示/消失的數(shù)據(jù)。

2-LiveData-Once-View.png

我們不應(yīng)該嘗試用 Architecture Components 基礎(chǔ)或擴(kuò)展庫來解決這個(gè)問題,相反這是一個(gè)設(shè)計(jì)問題。我們建議你將這些事件作為數(shù)據(jù)狀態(tài)的一部分。在本文中,我們將展示一些常見錯(cuò)誤和推薦方法。

? Bad: 1. Using LiveData for events

這種用法是在 LiveData 中保存一個(gè) SnackBar 消息,或一個(gè) Navigation 事件。盡管原則上是 LiveData 的正常使用,但這存在一些問題。
在一個(gè)包含首頁和詳情頁的應(yīng)用中,首頁的 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 中的值會(huì)永遠(yuǎn)為 true,從而導(dǎo)致無法回到首頁。
復(fù)現(xiàn)步驟是:

  1. 用戶點(diǎn)擊按鈕,啟動(dòng)詳情頁 DetailsActivity
  2. 用戶點(diǎn)擊返回鍵,返回到主界面 MasterActivity
  3. 這時(shí) MasterActivity 由非活動(dòng)狀態(tài)恢復(fù)到活動(dòng)狀態(tài)
  4. myViewModel 觀察到 _navigateToDetails 仍舊為 true,就又跳轉(zhuǎn)到詳情頁 DetailsActivity

一種看起來沒問題的解決方案是:頁面跳轉(zhuǎn)后立馬把標(biāo)志位設(shè)為 false,如 ListViewModel.kt 所示:

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

然而,需要注意的是:LiveData 不能保證發(fā)射它接收到的每個(gè)數(shù)據(jù)值。例如我們?cè)跊]有活動(dòng)的觀察者時(shí)設(shè)置了一個(gè)新值,這個(gè)新值不會(huì)被發(fā)送,此外,在多個(gè)子線程中操作 LiveData 可能發(fā)生競(jìng)爭(zhēng)狀況,從而導(dǎo)致觀察者只會(huì)收到一次回調(diào)。
但這個(gè)方案的主要問題是:別人很難看懂這個(gè)代碼,并且這種代碼也很丑陋。那么,我們應(yīng)該怎么確保在導(dǎo)航事件發(fā)生后恢復(fù)初值呢?

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

另一種稍微好點(diǎn),但仍有問題的方案是:View 告訴 ViewModel,導(dǎo)航事件已經(jīng)完成,LiveData 應(yīng)該恢復(fù)默認(rèn)值了。

Usage

基于第一節(jié)的例子,對(duì)觀察者代碼做如下改動(dòng)即可,MyFragment.kt

listViewModel.navigateToDetails.observe(this, Observer {
    if (it) {
        myViewModel.navigateToDetailsHandled()
        startActivity(DetailsActivity...)
    }
})

然后在 ListViewModel.kt 中添加一個(gè) 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 中每添加一個(gè)事件都要添加一個(gè)對(duì)應(yīng)的方法,并且很容易出錯(cuò)。此外,觀察者(View)很容易忘記調(diào)用 ViewModel 的這個(gè)方法。

? OK: Use SingleLiveEvent

一種還可以接受的解決方案是:SingleLiveEvent。這個(gè)類是 Google 官方 Demo 中的適用于這種特殊場(chǎng)景的解決方案,它是一個(gè)僅發(fā)送一次更新的 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 的問題在于:它僅限于一個(gè)觀察者。如果你無意中添加了多個(gè),則只會(huì)有一個(gè)收到回調(diào),并且無法保證哪一個(gè)會(huì)收到。

3-LiveData-SingleLiveEvent-Issue.png

? Recommended: Use an Event wrapper

推薦的解決方案是:封裝事件。通過這種方式,我們可以明確地管理實(shí)踐是否被處理,從而減少錯(cuò)誤。

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...)
    }
})

這種方案的優(yōu)勢(shì)在于:用戶需要調(diào)用 Event#getContentIfNotHandled() 方法或 Event#peekContent() 來指定跳轉(zhuǎn) Intent。這種方案將事件作為 UI 狀態(tài)的一部分:現(xiàn)在它們只是一個(gè)已被消費(fèi)或未被消費(fèi)的消息。

4With an Event wrapper, you can add multiple observers to a single-use event

總結(jié)

design events as part of your state. 我們可以包裝自己的 Event 來滿足自己的需求。
Bonus! 如果有很多事件,可以使用 EventObserver 避免一些樣板代碼。

參考

聯(lián)系

我是 xiaobailong24,您可以通過以下平臺(tái)找到我:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容