【譯】LiveData 使用詳解

xiaobailong24發表於2019-03-04

前言

本文翻譯自【Understanding LiveData made simple】,詳細介紹了 liveData 的使用。感謝作者 Elye。水平有限,歡迎指正討論。
Architecture Components 可以說是 Google 提供給 Android 開發者的一大福利。LiveData 是其中的一個主要元件,下面我們一起看下該怎麼使用好 LiveData
如果你之前沒了解過 Architecture Components,可以看下作者的另一篇文章:Android Architecture Components for Dummies in Kotlin (50 lines of code)
在上一篇文章中,作者提到了 ViewModelLiveData,其中 LiveData 是用來從 ViewModel 層向 View 層傳遞資料。但當時並沒有完整地介紹 LiveData 的作用,現在我們來詳細看下 LiveData 的定義和使用。
那麼,LiveData 有什麼特別的地方呢?

正文

什麼是 LiveData

官方 定義是:

LiveData 是一個可被觀察的資料持有類。與普通的被觀察者(如 RxJava 中的 Observable)不同的是,LiveData 是生命週期感知的,也就是說,它能感知其它應用元件(Activity,Fragment,Service)的生命週期。這種感知能力可以確保只有處於 active 狀態的元件才能收到 LiveData 的更新。詳情可檢視 Lifecycle

這就是官方對 LiveData 的定義。
為了簡單起見,我們先來看一些之前的開發習慣,以便更好地理解。

起源

當 Android 剛誕生的時候,大多數開發者寫的程式碼都放在一個 Activity 中。

1-All-in-one-Activity.png

然而,把所有邏輯都放在一個 Activity 類中並不理想。因為 Activity 很難進行單元測試。
鑑於此,業界出現了MVC、MVP、MVVM 等開發架構,通過 Controller、Presenter、ViewModel 等分層抽離 Activity 中的程式碼。

2-Presenter-ViewModel.png

這種架構能把邏輯從 View 層分離出來。然而,它的問題是 Controller、Presenter、ViewModel 等不能感知 Activity 的生命週期,Activity 的生命週期必須通知這些元件。
為了統一解決方案,Google 開始重視這個問題,於是 Architecture Components 誕生了。
其中 ViewModel 元件有一個特殊能力,我們不需要手動通知它們,就可以感知 Activity 的生命週期。這是系統內部幫我們做的事情。

3-Lifecycle-LiveData-ViewModel.png

除了 ViewModel 外,用於從 ViewModel 層暴露到 View 層的資料,也有生命週期感知的能力,這就是為什麼叫做 LiveData 的原因。作為一個被觀察者,它可以感知觀察它的 Activity 的生命週期。

舉例說明

為了更好地理解,下圖將 LiveData 作為資料中心:

4-LiveData-Center.png

從上圖可以看到,LiveData 的資料來源一般是 ViewModel,或者其它用來更新 LiveData 的元件。一旦資料更新後,LiveData 就會通知它的所有觀察者,例如 Activity、Fragment、Service 等元件。但是,與其他類似 RxJava 的方法不同的是,LiveData 並不是盲目的通知所有觀察者,而是首先檢查它們的實時狀態。LiveData 只會通知處於 Actie 的觀察者,如果一個觀察者處於 PausedDestroyed 狀態,它將不會受到通知。
這樣的好處是,我們不需要在 onPauseonDestroy 方法中解除對 LiveData 的訂閱/觀察。此外,一旦觀察者重新恢復 Resumed 狀態,它將會重新收到 LiveData 的最新資料。

LiveData 的子類

LiveData 是一個抽象類,我們不能直接使用。幸運的是,Google 提供了一些其簡單實現,讓我們來使用。

MutableLiveData

MutableLiveData 是 LiveData 的一個最簡單實現,它可以接收資料更新並通知觀察者。
例如:

// Declaring it
val liveDataA = MutableLiveData<String>()

// Trigger the value change
liveDataA.value = someValue

// Optionally, one could use liveDataA.postValue(value)
// to get it set on the UI thread
複製程式碼

觀察 LiveData 也很簡單,下面展示了在 Fragment 中訂閱 LiveDataA

class MutableLiveDataFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        getLiveDataA().observe(this, changeObserver)
    }
    // .. some other Fragment specific code ..
}
複製程式碼

結果如下,一旦 LiveDataA 資料發生變化(例如7567和6269),Fragment 就會收到更新。

5-MutableLiveData.gif

上面的程式碼中有這麼一行:

getLiveDataA().observe(this, changeObserver)
複製程式碼

這就是訂閱 LiveData 的地方,但是並沒有在 Fragment pausingterminating 時解除訂閱。
即使我們沒有解除訂閱,也不會有什麼問題。看下面的例子,當 Fragment 銷燬時,LiveData 不會因為產生一個新資料(1428)通知給 inactive 的 Fragment 而崩潰(Crash)。

6-MutableLiveData-inactive-Fragment.gif

同時,也可以看到當 Fragment 重新 active 時,將會收到最新的 LiveData 資料:1428。

Transformations#map()

我們一般定義一個 Repository 負責從網路或資料庫獲取資料,在將這些資料傳遞到 View 層之前,可能需要做一些處理。
如下圖,我們使用 LiveData 在各個層之間傳遞資料:

7-Transformations.Map-Repository-LiveData.png

我們可以使用 Transformations#map() 方法將資料從一個 LiveData 傳遞到另一個 LiveData。

class TransformationMapFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        val transformedLiveData = Transformations.map(
                getLiveDataA()) { "A:$it" }
        transformedLiveData.observe(this, changeObserver)
    }
    // .. some other Fragment specific code ..
}
複製程式碼

結果如下所示,以上程式碼將 LiveDataA 的資料(5116)進行處理後變為 A:5116

8-Transformations.Map-Sample.gif

使用 Transformations#map() 有助於確保 LiveData 的資料不會傳遞給處於 dead 狀態的 ViewModel 和 View。

9-Transformations.Map-Helpful.png

這很酷,我們不用擔心解除訂閱。
下面來看下 Transformations#map() 的原始碼:

@MainThread
public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
        @NonNull final Function<X, Y> func) {
    final MediatorLiveData<Y> result = new MediatorLiveData<>();
    result.addSource(source, new Observer<X>() {
        @Override
        public void onChanged(@Nullable X x) {
            result.setValue(func.apply(x));
        }
    });
    return result;
}
複製程式碼

這裡用到了 LiveData 的另一個子類 MediatorLiveData。接下來看一看這是個什麼東西。

MediatorLiveData

Transformations#map() 原始碼中可以看到,MediatorLiveData 有一個 MediatorLiveData#addSource() 方法,這個方法改變了資料內容。
也就是說,我們可以通過 MediatorLiveData 將多個 LiveData 源資料集合起來,如下圖所示:

10-MediatorLiveData.png

程式碼如下:

class MediatorLiveDataFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        val mediatorLiveData = MediatorLiveData<String>()
        mediatorLiveData.addSource(getliveDataA())
              { mediatorLiveData.value = "A:$it" }
        mediatorLiveData.addSource(getliveDataB())
              { mediatorLiveData.value = "B:$it" }
        mediatorLiveData.observe(this, changeObserver)
    }
    // .. some other Fragment specific code ..
}
複製程式碼

這樣,Fragment 就可以同時接收到 LiveDataALiveDataB 的資料變化,如下圖所示:

11-MediatorLiveData-Sample.gif

有一點需要注意的是:當 Fragment 再處於 active 狀態時,如果 LiveDataALiveDataB 的資料都發生了變化,那麼當 Fragment 重新恢復 active 狀態時,MediatorLiveData 將獲取最後新增的 LiveData 的資料傳送給 Fragment,這裡即 LiveDataB

12-MediatorLiveData-Sample-2.gif

從上圖可以看到,當 Fragment 恢復活動狀態時,它就會收到 LiveDataB 的最新資料,無論 LiveDataB 變化的比 LiveDataA 變化的早或晚。從上面程式碼可以看到,這是因為 LiveDataB 是最後被新增到 MediatorLiveData 中的。

Transformations#switchMap

上面的示例中展示了我們可以同時監聽兩個 LiveData 的資料變化,這是很有用的。但是,如果我們想要手動控制只監聽其中一個的資料變化,並能根據需要隨時切換,這時應怎麼辦呢?
答案是:Transformations#switchMap(),Google 已經為我們提供了這個方法。它的定義如下:

@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
        @NonNull final Function<X, LiveData<Y>> func) {
    final MediatorLiveData<Y> result = new MediatorLiveData<>();
    result.addSource(trigger, new Observer<X>() {
        LiveData<Y> mSource;

        @Override
        public void onChanged(@Nullable X x) {
            LiveData<Y> newLiveData = func.apply(x);
            if (mSource == newLiveData) {
                return;
            }
            if (mSource != null) {
                result.removeSource(mSource);
            }
            mSource = newLiveData;
            if (mSource != null) {
                result.addSource(mSource, new Observer<Y>() {
                    @Override
                    public void onChanged(@Nullable Y y) {
                        result.setValue(y);
                    }
                });
            }
        }
    });
    return result;
}
複製程式碼

這個方法用來新增一個新資料來源並相應地刪除前一個資料來源。因此 MediatorLiveData 只會包含一個 LiveData 資料來源。這個控制開關也是一個 LiveData。整個過程如下所示:

13-Transformations.switchMap-Sample.gif

使用方法如下:

class TransformationSwitchMapFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)

        val transformSwitchedLiveData =
            Transformations.switchMap(getLiveDataSwitch()) {
                switchToB ->
                if (switchToB) {
                     getLiveDataB()
                } else {
                     getLiveDataA()
                }
        }

        transformSwitchedLiveData.observe(this, changeObserver)
    }
    // .. some other Fragment specific code ..
}
複製程式碼

這樣,我們就能很容易地控制用哪個資料來更新 View 檢視,如下所示,當正在觀察的 LiveData 發生變化,或者切換觀察的 LiveData 時,Fragment 都會收到資料更新。

14-Transformations.switchMap.png

一個實際的使用場景是,我們可以通過特定設定(如使用者登入 session)的不同資料來源,來處理不同的業務邏輯。

原始碼地址

以上示例程式碼可以在作者的 Github 上找到:github.com/elye/demo_a…
下載原始碼檢視,能更好地理解。

更多示例

如果訂閱了一個 LiveData,但又不想收到資料更新的通知,可以參考一下文章:
LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)

參考

聯絡

我是 xiaobailong24,您可以通過以下平臺找到我:

相關文章