用 LiveData 處理畫面效果與事件

aka. SingleLiveEvent

Ray Yuan Liou
4 min readFeb 3, 2021
No!

MVVM (Model-View-ViewModel) 是近來 Android 開發流行的架構,使用觀察者模式在 Presentation 層和 View 溝通。
有幾種作法可以達到效果:

  • 透過 RxJava Subject
  • 透過 LiveData (也是官方推薦的作法)
  • 手作觀察者模式 ( OnClickListener也是一種觀察者模式作法)

這裡討論的是官方建議的 LiveData 實作方式,View 去 observe ViewModel 暴露的 LiveData,根據不同的畫面狀態 (ViewState) 進行畫面處理。
LiveData 的好處是幫忙處理了 Android Context 生命週期的應對,交給他做不必煩惱。除了避免了忘記處理 Reference 造成的 Memory Leak,也不會發生 Activity 在 backStack 中接受到訊號回魂的奇異事件。

也因為 LiveData 自身的實作,產生了額外的問題,這也是本文想探討的 —LiveData 會在 onConfigurationChange() 或其他生命週期更動後重新訂閱,LiveData 會將上一筆資料再丟出去。 (嘿聽起來就像是 RxJava 的 BehaviorSubject
如果我們把顯示對話框、Toast 或 SnackBar 等 UI 元件的顯示事件(下面會把這些 Event 稱作 ViewEffect Event)也透過 LiveData 送出去,使用者就可能會遇到一些怪怪的現象,像是 Toast 重複出現之類的。另一方面,透過 LiveData 實作 EventBus,或是透過 LiveData 送 State 的 MVI (Model-View-Intent) 架構,也會遇到這個問題。

重送了是誰的錯?

筆者在 Code Review 時被同事叮嚀這個可能會出現的問題,查完資料後覺得挺適合分享出去避免其他人也誤觸雷區。這邊的心法似乎就是把 LiveData 善意的重送冷血無視。

在 Google 官方架構範例專案 (architecture-samples) 之中,提供了一個修改原有 LiveData 的作法 (說也奇怪,都自己人了還需要繞道嗎?😕 )透過 flag 卡住已經重複送過的事件。

這種作法會導致只有一個 observer 會收到事件,因為收到事件後避免送下一次的 flag 就被鎖上。以下是照著官方 Sample 改寫的範例檔案:

官方範例簡略改寫,並由 Java 改為 Kotlin

可以發現 setValue時,pendingflag 會被設為 true。在資料更動後通知訂閱者的地方,可以注意到裡頭又多建立了一個 Observer,並且比對 pending的值是否為 true,是 true 除了會真的拋出事件,也會將 pending改為 false 鎖住。

因為這個寫法,造成除了第一個 observer 可以收到事件,其他訂閱者將會聽不到。

救出事件大作戰

Jose Alcérreca 提供了另一個解套方式,Jose 是 architecture sample SingleLiveData的作者。如果最近你也跟我一樣去找 SingleLiveData的程式碼,你會發現他把它全部註解掉了。😂

todo-mvvm-live-kotlin branch,官方已經不推薦使用這個方法來送一次性事件。
現在官方 sample 的作法,是將此類不希望因為生命週期變化造成的事件包在新建的 Event class 之下。這個 class 裡頭設一個 flag 去決定該送原數值或空值,並且交由訂閱者 null check 來決定該不該執行。

Event class 大致是這個樣子

有了 Event class 之後,就可以來包裝 ViewEffect Class 了 (或是任何你想包裝成只反應一次的物件)。
以下範例示範定義 ViewEffect class,接著再透過 LiveData 拋出去。

範例是請 View 顯示 Toast,裡頭顯示 Hello!

在 View 這邊,只要去檢查 LiveData 送出來的數值是否為空,即可以避免生命週期變化造成事件重送。

取出 ViewEffect 後可以透過 when 表達式處理細部流程

這樣就能享受 LiveData 帶來的便利,同時不想被二次處理的事件也不會被二度處理

這些好文章也看看

--

--