Android: Flow vs RxJava vs LiveData
這麼多 reactive stream 是要用哪一個?現在還多一個 Compose State
Flow
相關名詞
- producer: 推送數值
- consumer: 收集消化 producer 製造的數值
Flow 原生支援 back pressure (像是 RxJava 2+),可以處理 producer 短時間內傳送大量資料。
Flow 本質是一種 Cold Flow,只有當有其他人 Collect (RxJava 叫做 subscribe),他才會開始做事。 (這邊跟 RxJava 的 Observable
/Single
等 reactive stream ... 類似)
例如,以下實作是一個倒數計時的 Flow
// producer
val countDownFlow = flow<Int> {
val startValue = 10
var currentValue = startValue
emit(startValue)
while (currentValue > 0) {
delay(1000L) // stop a second
currentValue--
emit(curretValue)
}
}
我們有多種不同的 collect 方式,例如:
// consumers
// assume we're in a ViewModel
private fun collectFlow() {
viewModelScope.launch {
countDownFlow.collect { time ->
println("time is $time)
}
}
}
也可以這樣:
private fun collectFlow() {
countDownFlow.onEach { time ->
println("time is $time)
}.launchIn(viewModelScope)
}
onEach
有點像 RxJava doOnNext
,在每次收到資料時都會被觸發。
Cancel Handling
.onCompletion { error ->
if (error is CancellationException) {
Log.i("MyCoolViewModel", "fetchCoolBanners is canceled")
}
}
Kotlin Flow operator
reduce vs fold
fold 就是「有預設值」的 reduce,行為基本上都是累加前一個累積數值。如果給一個空 Collection 給它 fold,他會直接回傳預設值。
flatMapConcat vs flatMapMerge
其實就是 RxJava flatMap 和 concatMap 的差別,flatMapConcat
有順序性而 flatMapMerge
則無,會一次打出去。Kotlin 官方這邊建議用 flatMapConcat
就好,少用 flatMapMerge
。
delay
上游 Flow emit 資料後,會等待下游 collect 結束後再往下走,換句話說 collect 裡有 delay 他就會等多久。 (這即是 back pressure 的體現:一般使用下的 Flow producer 和 consumer 的 code block 是跑在同一個 coroutines 裡面的!)
buffer vs conflate
這時如果不想讓上游 Flow 等待,可以用 buffer 這個 operator。如此一來 collect 的 scope 會跑在不同的 coroutines,上游也不會等下游 collect 做完,會持續發射數值。
但如果 buffer 的 size 不足,可能會導致 overflow,需要慎用。
conflate 也會繼續走下去,不過如果在發射新的數值後,下遊來不及 collect 就會直接被跳過。透過 conflate 蒐集的 flow 會在上一個數值完成 collect 之後,去取得最新被射出的數值。(也就是最後一個 emit 的數值)
根據這兩個 operator 的作用,我們可以再次體現到一般使用下的 Flow producer 和 consumer 的 code block 就是跑在同一個 coroutines 裡面的!
collectLatest
如果有新的資料吐出來,會放棄目前正在處理的資料,去 collect 最新的資料。
Hot Flows
與 Flow 本體不同,就算沒有人 collect,也可以持續的送出資料。
相對於 Cold Flow,拿個比方來說:
Cold Flow 就像是 CD/DVD,需要放到播放器裡才會播放內容;Hot Flow 則像是廣播/電視頻道,無論有沒有人收聽都會持續放送。
或 Cold Flow 像 YouTube 裡頭的影片,不點進去不會播;Hot Flow 則像是直播,無論有沒有人收看都會持續播。
StateFlow
基本上是失去察覺 Lifecycle 能力的 LiveData,會存最後一個 Data,在 collect 的瞬間也會吐上一個最近的 Data 給你。(就像 LiveData!)
例如,我們設定讓一個 StateFlow 的 ViewEvent 顯示一個 Toast,當裝置旋轉時,會再顯示出來一次。因為這邊 Activity 重新 recreate 後重新 collect 一次,stateFlow 又暴露出上一個值。
需要注意的是,stateFlow 正常使用且 Activity 無 recreate()
的情況下,他是不會吐相同的資料的。
any instance of StateFlow already behaves as if
distinctUntilChanged
operator is applied to it.
此外,StateFlow 需要給一個預設值。
因為他不知道 App 目前的狀態,所以在背景也是 active 的 (LiveData 則否),但可以透過方法讓他表現的很像 LiveData。
以下是類似的 Stream:
可以這樣 collect StateFlow,讓他行為像是 LiveData:
lifecycleScope.launch {
// you can change to any lifecycle state you want
// we set to STARTED here to emulated behaviors as a LiveData
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateFlow.collectLatest {
// collect to your data here
}
}
}
要注意的是,官方已經不建議這樣 collect: lifecycleScope.launchWhenStarted
,連文件都移除了。
SharedFlow
用來做 One-Time Event,不需要給他預設值。
可以透過建構子的 replay 參數來設定 Cache,代表可以將同一個值 Keep 幾次。而且 SharedFlow 並沒有 distinctUntilChanged
的特性,可以重複吐一樣的資料。
它需要在 coroutines 裡頭 emit value。
技術選擇
LiveData vs StateFlow
兩者可以做到幾乎相同的事情
choose LiveData
- 專案使用 Java
choose StateFlow
- 更好寫 unit test
- 沒有與 Android Context 綁太深,以及可以在 Kotlin coroutines 的基礎上做測試
- 更強大的 flow operators
Suspend function vs Flow vs StateFlow vs SharedFlow
choose Suspend function
- 非同步工作只需要回傳 一個資料
choose Flow
- 非同步工作可以連續回傳資料,或回傳多個資料
choose StateFlow
- 傳送 State 到你的 UI,當作是 LiveData 的替代品
choose SharedFlow
- 傳送一次性事件到你的 UI,當作是 SingleLiveEvent 的替代品
StateFlow/SharedFlow vs Compose State
本比較基於 UI Layer (VM <-> View) 的溝通上
choose StateFlow/SharedFlow
- 專案 UI 建立於 XML
- compose 並不支援 StateFlow/SharedFlow,需要透過
collectAsState()
來監聽。會失去原來 SharedFlow 的特性。(變得不是一次性 Event,他會變成 Compose State)所以不推薦 compose 搭配 SharedFlow 使用 - Compose 已經有自己的 State holder,compose 搭配 StateFlow 使用需要轉換。
2024/02/29 Update: Perry Lu 提到的 StateFlow 留言也很棒,記得看留言區!
choose Compose State
- 專案 UI 建立於 Compose
Compose State 是唯一的 State holder
- compose 依賴於 compose state 裡頭的狀態變化來做 recomposition (重繪)
在 Compose 裡頭 collect SharedFlow
用 LaunchedEffect
來做。
setContent {
demoAppTheme {
val viewModel = viewModel<DemoViewModel>()
val context = LocalContext.current
LaunchedEffect(key1 = true) {
viewModel.sharedFlow.collect { viewEffect ->
// collect one-time event here
if (viewEffect is ShowSnackBar) {
SnackBar.make(context, viewEffect.text, SnackBar.LONG).show()
}
}
}
Box {
Text("Hello world", modifier.centerVertical())
}
}
}
如何測試 Flow?
可以用 turbine 用來對 Flow 做 Unit Test