LiveData & Flow.asLiveData() 的差異

Connie Lin
11 min readFeb 17, 2023

Hot flow 有自己的資源管理行為,例如幾秒都沒有 collector 就會進 inactive、shared flow 也考慮當下是否有 collector 來決定是否把資料放進 buffer。很好奇這些特性在 asLiveData 之後會發生什麼事,也好奇 asLiveData 究竟是在哪些時間點 collect flow、如何處理資料通知的時間點,於是就研究了一下

LiveData 的 observe

LiveData<T>.observe(LifecycleOwner, Observer)

LiveData 有管理通知資料變化的生命週期,只會在 lifecycle owner active 的期間(started, resumed),才會通知 observer,避免在沒有畫面的期間去取用 view 相關的物件:

Owner inactive 期間的資料異動不會通知觀察者,直到 owner 恢復 active,observer 才會接收到最新的一筆資料,因此很適合用於畫面顯示。

使用 flowWithLifecycle 來取得用於畫面顯示的 flow data

Flow 本身並沒有限制生命週期,如果資料是要用於畫面顯示,可以使用 flowWithLifecycle,會幫忙在每次進入特定生命週期時 collect data,這個 API 的 State 預設為 State.STARTED,適用於畫面顯示。

但因為 hot flow never ends,每個 hot flow 要在個別的 lifecycle scope collect 才不會阻塞,加上還得自己加 flowWithLifecycle才能限縮 lifecycle,有些人可能更喜歡在 asLiveData 之後改用觀察的方式。

// 直接 collect flow

LifecycleScope.launch {
hotFlowA.flowWithLifecycle(lifecycle)
.collect { ... }
}
LifecycleScope.launch {
hotFlowB.flowWithLifecycle(lifecycle)
.collect { ... }
}

// 轉為 LiveData 之後改用 observe

hotFlowA.asLiveData().observe(viewLifecycleOwner) {
...
}
hotFlowB.asLiveData().observe(viewLifecycleOwner) {
...
}

那就來看一下 Flow.asLiveData 有哪些注意事項吧。

Flow.asLiveData.observe 是怎麼做 collect 的?

asLiveData() 會創建一個 LiveData,在 block 中 collect flow,取得新資料時,會呼叫底層 LiveDataScopeImplemit,將 emitted data 設定為 LiveData 的值,藉此將 Flow 轉成 LiveData。底層的 CoroutineLiveData 也會去控制只在 LiveData onActive() 的時候才開始 collect,並在 onInactive() 達 timeout 時,取消 flow collection。

大多的行為在底層都處理好了,篇幅關係就不貼出來了,有興趣的話可以去翻一下 code。關於 asLiveData,我們要知道的只有:

  • 透過 asLiveData() 將 Flow 轉為 LiveData,會在 active 期間開始 collect flow,並在 inactive timeout 之後取消 flow collection。
  • asLiveData() 傳入timeoutInMs 可以設定 timeout 時長,預設為五秒。

LiveData 和 Flow.asLiveData 有什麼差異?

如果在 owner inactive 的期間 set value 的話⋯

LiveData.setValue

LiveData 本身的特性會把 pending 的資料暫存起來,直到 active state change 之後才會通知,恢復觀察時就可以拿到一筆最新的資料

StateFlow.setValue (hot flow)

StateFlow 的特性是可以存放一筆最新的 replay cache,因此資料會被暫存起來,直到 owner 恢復 active,asLiveData() 開始 collect flow 之後,observer 就可以拿到 replay cache 了,因此觀察時也可以拿到一筆最新的資料。

SharedFlow.emit(hot flow)

asLiveData() 只會在 owner active 期間去 collect,如果將 SharedFlow 的 replay 設定為 0(預設值就是如此),是不會拿到先前的資料的,因此沒有設定 replay,就不會透過 SharedFlow 拿到 inactive 期間的資料。

不過有個要注意的地方是,前面提到 asLiveData 持續 inactive 五秒以上,才會取消 flow collection,而在 inactive 未達五秒的期間,flow 會將新資料 emit 給 LiveData,在 inactive 的狀態下,LiveData 不會通知 observer,而是把最新一筆 pending 資料暫存起來。恢復 active state 後,observer 拿到的資料是由 LiveData 暫存的,而非 shared flow 的 cache。

❗️ asLiveData() 讓我們有可能拿到由 LiveData 暫存的資料,但因為 inactive timeout 後 sharedFlow 的資料不會被記錄,有機會發生不預期的狀態(你以為的最新其實不是最新)️❗

Flow(cold flow)

Cold flow 不能彈性 emit value,只會從定義好的行為中 emit 資料,並由 collect trigger,每一次的 collect 都會從頭 trigger 一次 cold flow 的流程。

雖然 asLiveData() 設定了 timeoutInMs,inactive timeout 前的資料會 emit 給 LiveData,但因為下一次恢復 active 的時候又會從頭 trigger 一次 cold flow,inactive 期間的資料其實不會被收到。

整理一下

非 hot flow 的 Flow.asLiveData().collect 會在每次 active 的時候重新 trigger flow,沒有 cache data 的問題,因此不在討論之列。下面表列了 LiveData、StateFlow 與 SharedFlow 不同情境下的行為。

首先,LiveData 和 StateFlow 使用以下方法來取得資料,體感是一樣的,差別在 StateFlow 確保會有預設值。

LiveData.observe
StateFlow.asLiveData().observe
StateFlow.flowWithLifecycle(State.Started).collect

如果是為了管理某些一次性的事件使用 SharedFlow.asLiveData() ,以下方式得到的資料可能會有差異,因此要注意使用情境會不會有 side effect。

SharedFlow.asLiveData().observe
SharedFlow.flowWithLifecycle(State.Started).collect

如果期望不要拿到先前的資料而使用 SharedFlow(replay = 0) ,直接 collect flow 會是比較乾淨的狀態,因為 LiveData 會把 inactive 未達 timeout 期間的最新一筆資料暫存起來。

LiveData・StateFlow・SharedFlow,兜幾?

如果資料需要用於畫面顯示⋯

只關心一筆最新的資料的話,LiveData & StateFlow 都可以。觀察端使用 observe 會比較簡潔,生產端只做 setValue 的話,LiveData 也會比較簡單,根據情境需求,隨喜選用就可以。

另外,生產端需要因應多個來源產生資料變化的話,可以考慮使用:

  • LiveData:有順序關係可使用 Transformation.map(source) ,因應 source 的改變去做 map,無順序關係可使用 MediatorLiveData 加入多個 source。
  • StateFlow :使用 flow 的 operators 可以更彈性的處理資料源之間的因果、順序關係,且如果需參考兩個以上的資料源,或是資料源之間有複雜的運算的話,覺得用 flow 更適合。

用過即棄的一次性事件

使用 SharedFlow(replay = 0).collect,只有在 collect 期間有變動的資料才會收到通知,且當下即焚,錯過不再 🔥

asLiveData 之後會受限於 LiveData 的特性,replay = 1以外的情境都不適合使用 asLiveData,不管是 replay = 0 或是 replay > 1,都直接用 collect 會更為適合。

更簡潔地 collect hot flow

其實,如果是覺得 flow collection 毛很多而改用 LiveData 的話,我試著寫了一個 extension function 來簡化 collect hot flow 可能要寫很多行的痛點(其實我不知道夠不夠痛 😂)。

fun <T> Flow<T>.launchCollection(
lifecycleOwner: LifecycleOwner,
collector: FlowCollector<T>,
) = lifecycleOwner.lifecycleScope.launch {
this@launchCollection
.flowWithLifecycle(lifecycleOwner.lifecycle)
.collect(collector)
}

這個 extension 會幫忙 launch CoroutineScope,並且在 scope 裏面做了Flow.repeateOnLifecycle.collect。這麼一來 collect flow 會變得比較精簡,尤其是多個 hot flow 的情境下不需要再自己 launch 一堆 CoroutineScope。

lifecycleScope.launch {
stateFlow
.flowWithLifecycle(lifecycle)
.collect {
// TODO
}
}

stateFlow.launchCollection(viewLifecycleOwner) {
// TODO
}

同場加映:timeoutInMs 的好處?

預設為五秒,可以在旋轉畫面或是短暫離開 app 的時候,讓 upstream flow 保持 active。如果單純一條 StateFlow 或 SharedFlow 作為 upstream flow,切換 inactive → active 可能沒什麼影響,但如果 upstream 有組合了多條 flow,inactive → active 會從頭 trigger 所有的 operation,如果是 cold flow 的話,還會再跑一次 flow 的流程,因此資源管理還是挺重要的。

這些地方都可以設定 timeoutInMs,可以思考當下的使用情境是否需要:

  • Flow.asLiveData():預設值 5000 ms
  • Flow.stateIn() / Flow.sharedIn() :SharingStarted 設定為 WhileSubscribe,預設值為 0 ms

--

--