Android SingleLiveEvent Redux with Kotlin Flow

點(diǎn)擊上方藍(lán)字關(guān)注我,知識(shí)會(huì)給你力量

這個(gè)系列我做了協(xié)程和Flow開發(fā)者的一系列文章的翻譯,旨在了解當(dāng)前協(xié)程、Flow、LiveData這樣設(shè)計(jì)的原因,從設(shè)計(jì)者的角度,發(fā)現(xiàn)他們的問題,以及如何解決這些問題,pls enjoy it。
?從這篇文章大家可以了解到我們?cè)谑褂肔iveData和Flow時(shí),是如何一步步發(fā)現(xiàn)問題,并解決問題的,特別是站在設(shè)計(jì)者的角度來看這些問題,你會(huì)學(xué)到解決問題的一般方法。
?
自從Jose Alcérreca發(fā)表了他的文章 "SingleLiveEvent Case "以來,已經(jīng)過去了好幾年。這篇文章對(duì)許多開發(fā)者來說是一個(gè)很好的起點(diǎn),因?yàn)樗屗麄兯伎糣iewModels和相關(guān)視圖(無論是Fragment還是Activity)之間的不同通信模式。
這篇文章可以看這里。https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
對(duì)于SingleLiveEvent案例,已經(jīng)有許多關(guān)于如何改進(jìn)該模式的回應(yīng)。我最喜歡的一篇文章是由 Hadi Lashkari Ghouchani的文章https://proandroiddev.com/livedata-with-single-events-2395dea972a8
然而,上面提到的兩種情況仍然使用LiveData作為備選的數(shù)據(jù)Store。我覺得仍有改進(jìn)的余地,尤其是在使用Kotlin的coroutines和flow時(shí)。在這篇文章中,我將描述我如何處理一次性事件,以及如何在Android生命周期中安全地觀察這些事件。
Background
為了與其他關(guān)于SingleLiveEvent的文章,或者說使用該模式的變體文章保持一致,我將把事件定義為采取一次、且僅一次行動(dòng)的通知。最初的SingleLiveEvent文章以顯示SnackBar為例,但你也可以把其他一次性動(dòng)作,如Fragment導(dǎo)航、啟動(dòng)Activity、顯示通知等作為「事件」的例子。
在MVVM模式中,ViewModel和它相關(guān)的視圖(Fragment或Activity)之間的通信通常是通過遵循觀察者模式來完成的。這使得視圖模型與視圖解耦,允許視圖經(jīng)歷各種生命周期狀態(tài),而不需要向觀察者發(fā)送數(shù)據(jù)。
在我的ViewModels中,我通常會(huì)公開兩個(gè)流來進(jìn)行觀察。第一個(gè)是視圖狀態(tài)。這個(gè)數(shù)據(jù)流定義了用戶界面的狀態(tài)。它可以被反復(fù)觀察,并且通常由Kotlin StateFlow、LiveData或其他類型的數(shù)據(jù)存儲(chǔ)來支持,暴露出一個(gè)單一的值。但是我將會(huì)忽略這個(gè)流程,因?yàn)樗皇潜疚牡闹攸c(diǎn)。然而,如果你感興趣的話,有很多文章描述了如何用StateFlow或LiveData實(shí)現(xiàn)UI狀態(tài)。
第二個(gè)可觀察流,也是本文的重點(diǎn),要有趣得多。這個(gè)數(shù)據(jù)流的目的是通知視圖執(zhí)行一個(gè)動(dòng)作,而且只有一次。比如說,導(dǎo)航到另一個(gè)Fragment。讓我們探討一下這個(gè)流程有哪些需要注意的地方。
Requirements
可以說,事件是重要的,甚至是關(guān)鍵的。所以讓我們?yōu)檫@個(gè)流程和它的觀察者定義一些要求。
新事件不能覆蓋未觀察到的事件。 如果沒有觀察者,事件必須緩沖到觀察者開始消費(fèi)它們。 視圖可能有重要的生命周期狀態(tài),在此期間它只能安全地觀察事件。因此,觀察者可能并不總是在某個(gè)特定的時(shí)間點(diǎn)上Activity或消費(fèi)流。
A Safe Emitter of Events
因此,滿足第一個(gè)要求,很明顯,一個(gè)流是必要的。LiveData或任何conflates Kotlin flow,如StateFlow或ConflatedBroadcastChannel,都不合適。一組快速發(fā)射的事件可能會(huì)相互覆蓋,而只有最后一個(gè)事件被發(fā)射到觀察者那里。
那么使用SharedFlow呢?這能幫助嗎?不幸的是,不能。SharedFlow是熱的。這意味著在沒有觀察者的時(shí)期,比如說在配置改變的時(shí)候,發(fā)射到流中的事件會(huì)被簡(jiǎn)單地丟棄。遺憾的是,這也使得SharedFlow不適合發(fā)射事件。
那么,我們有什么辦法來滿足第二和第三個(gè)要求呢?幸運(yùn)的是,一些文章已經(jīng)為我們描述過了。
JetBrains的Roman Elizarov寫了一篇關(guān)于各種類型流量的不同使用情況的文章。
這篇文章中特別有趣的是 "A use-case for channels "一節(jié),他描述了我們所需要的東西——一個(gè)單次事件總線,是一個(gè)緩沖的事件流。文章地址如下:https://elizarov.medium.com/shared-flows-broadcast-channels-899b675e805c
?......channels也有其應(yīng)用場(chǎng)合。channels被用來處理那些必須被精確處理一次的事件。這發(fā)生在一個(gè)設(shè)計(jì)中,有一種類型的事件通常有一個(gè)訂閱者,但間歇性地(在啟動(dòng)或某種重新配置期間)根本沒有訂閱者,而且有一個(gè)要求,即所有發(fā)布的事件必須保留到一個(gè)訂閱者出現(xiàn)。
?
現(xiàn)在我們已經(jīng)找到了一種安全的方法來發(fā)射事件,讓我們用一些示例事件來定義一個(gè)ViewModel的基本結(jié)構(gòu)。
class MainViewModel : ViewModel() {
sealed class Event {
object NavigateToSettings: Event()
data class ShowSnackBar(val text: String): Event()
data class ShowToast(val text: String): Event()
}
private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()
init {
viewModelScope.launch {
eventChannel.send(Event.ShowSnackBar("Sample"))
eventChannel.send(Event.ShowToast("Toast"))
}
}
fun settingsButtonClicked() {
viewModelScope.launch {
eventChannel.send(Event.NavigateToSettings)
}
}
}
上面的例子中,視圖模型在構(gòu)建時(shí)立即發(fā)射了兩個(gè)事件。觀察者可能不會(huì)馬上消費(fèi)它們,所以它們被簡(jiǎn)單地緩沖,并在觀察者開始從Flow中collect時(shí)被發(fā)射出來。在上面的例子中,還包括了視圖模型對(duì)按鈕點(diǎn)擊的處理。
事件發(fā)射器的實(shí)際定義出乎意料的簡(jiǎn)單和直接。現(xiàn)在,事件的發(fā)射方式已經(jīng)定義好了,讓我們繼續(xù)討論如何在Android的背景下安全地觀察這些事件,以及不同的生命周期狀態(tài)帶來的限制。
A Safe Observer of Events
Android Framework強(qiáng)加給開發(fā)者的不同的生命周期可能很難處理。許多操作只能在某些生命周期狀態(tài)下安全地執(zhí)行。例如,F(xiàn)ragment導(dǎo)航只能在onStart之后、onStop之前進(jìn)行。
那么,我們?nèi)绾伟踩赜^察只在給定生命周期狀態(tài)下的事件流呢?如果我們觀察視圖模型的事件流,比如說一個(gè)Fragment,在Fragment提供的coroutine范圍內(nèi),這是否能滿足我們的需要?
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.eventsFlow
.onEach {
when (it) {
is MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
很遺憾,答案是否定的。viewLifecycleOwner.lifecycleScope的文檔指出,當(dāng)生命周期被銷毀時(shí),這個(gè)Scope會(huì)被取消。這意味著有可能在生命周期達(dá)到停止?fàn)顟B(tài)但尚未銷毀的情況下收到事件。如果在處理事件的過程中執(zhí)行諸如Fragment導(dǎo)航之類的操作,這可能會(huì)有問題。
使用launchWhenX的誤區(qū)
也許我們可以用launchWhenStarted來控制一個(gè)事件被接收的不同生命周期狀態(tài)?比如說。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get your view model here
lifecycleScope.launchWhenStarted {
viewModel.eventsFlow
.collect {
when (it) {
MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
}
}
不幸的是,這也有一些重大問題,特別是在配置變化方面。Halil Ozercan寫了一篇關(guān)于Android生命周期Coroutines的精彩深度文章,他描述了launchWhenX這組函數(shù)背后的基本機(jī)制。他在文章中指出。
?launchWhenX函數(shù)在生命周期離開期望狀態(tài)時(shí)不會(huì)被取消。它們只是被暫停了。只有當(dāng)生命周期達(dá)到DESTROYED狀態(tài)時(shí)才會(huì)取消。
我對(duì)他的文章進(jìn)行了回應(yīng),證明在任何 launchWhenX 函數(shù)中觀察一個(gè)流程時(shí),都有可能在配置改變時(shí)丟失事件。這篇回應(yīng)很長(zhǎng),我就不在這里重復(fù)了,所以我鼓勵(lì)你去讀它。
?
地址如下:https://themikeferguson.medium.com/pitfalls-of-observing-flows-in-launchwhenresumed-2ed9ffa8e26a
?關(guān)于這個(gè)問題的簡(jiǎn)明演示,見https://gist.github.com/fergusonm/88a728eb543c7f6727a7cc473671befc
?
因此,遺憾的是,我們也不能利用 launchWhenX 的擴(kuò)展函數(shù)來幫助控制一個(gè)流在什么生命周期狀態(tài)下被觀察。那么我們能做什么呢?退一步講,如果我們花點(diǎn)時(shí)間看看我們要做什么,我們可以更容易地找出一個(gè)解決方案,只在特定的生命周期狀態(tài)下進(jìn)行觀察。分解這個(gè)問題,我們注意到,我們真正想做的是在一個(gè)狀態(tài)下開始觀察,在另一個(gè)狀態(tài)下停止觀察。
如果我們使用另一個(gè)工具,比如RxJava,我們可以在onStart生命周期回調(diào)中訂閱事件流,并在onStop回調(diào)中進(jìn)行處置。(類似的模式也可以用于通用回調(diào))。
override fun onStart() {
super.onStart()
disposable = viewModel.eventsFlow
.asObservable() // converting to Rx for the example
.subscribe {
when (it) {
MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
}
override fun onStop() {
super.onStop()
disposable?.dispose()
}
為什么我們不能用Flow和coroutines做到這一點(diǎn)?嗯,我們可以。當(dāng)生命周期被破壞時(shí),作用域仍然會(huì)被取消,但是我們可以將觀察者處于Activity狀態(tài)的時(shí)間緊縮到只有啟動(dòng)和停止之間的生命周期狀態(tài)。
override fun onStart() {
super.onStart()
job = viewModel.eventsFlow
.onEach {
when (it) {
MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
override fun onStop() {
super.onStop()
job?.cancel()
}
這滿足了第三個(gè)要求,解決了只在安全生命周期狀態(tài)下觀察事件流的問題,但它引入了大量的模板。
「Cleaning Things Up」
如果我們把管理這項(xiàng)工作的責(zé)任委托給其他東西,以幫助消除這些模板,會(huì)怎么樣?Patrick Steiger的文章《用LiveData替代StateFlow或SharedFlow》中就有一個(gè)驚人的小插曲。(這也是一篇很好的讀物)。原文地址如下:https://proandroiddev.com/should-we-choose-kotlins-stateflow-or-sharedflow-to-substitute-for-android-s-livedata-2d69f2bd6fa5
他創(chuàng)建了一組擴(kuò)展函數(shù),當(dāng)一個(gè)生命周期的所有者達(dá)到開始時(shí),自動(dòng)訂閱一個(gè)流量Collect器,當(dāng)生命周期達(dá)到停止階段時(shí),取消Collect器。下面是我對(duì)其稍加修改的版本。
(2021年10月編輯:請(qǐng)看下面的更新版本,它利用了最近的庫(kù)變化。)
class FlowObserver<T> (
lifecycleOwner: LifecycleOwner,
private val flow: Flow<T>,
private val collector: suspend (T) -> Unit
) {
private var job: Job? = null
init {
lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver {
source: LifecycleOwner, event: Lifecycle.Event ->
when (event) {
Lifecycle.Event.ON_START -> {
job = source.lifecycleScope.launch {
flow.collect { collector(it) }
}
}
Lifecycle.Event.ON_STOP -> {
job?.cancel()
job = null
}
else -> { }
}
})
}
}
inline fun <reified T> Flow<T>.observeOnLifecycle(
lifecycleOwner: LifecycleOwner,
noinline collector: suspend (T) -> Unit
) = FlowObserver(lifecycleOwner, this, collector)
inline fun <reified T> Flow<T>.observeInLifecycle(
lifecycleOwner: LifecycleOwner
) = FlowObserver(lifecycleOwner, this, {})
使用這些擴(kuò)展功能是超級(jí)簡(jiǎn)單和直接的。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.eventsFlow
.onEach {
when (it) {
MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
.observeInLifecycle(this)
}
// OR if you prefer a slightly tighter lifecycle observer:
// Be sure to use the right lifecycle owner in each spot.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.eventsFlow
.onEach {
when (it) {
MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
.observeInLifecycle(viewLifecycleOwner)
}
現(xiàn)在我們有了一個(gè)事件觀察者,它只在達(dá)到開始的生命周期后進(jìn)行觀察,當(dāng)達(dá)到停止的生命周期時(shí),它就取消。
它還有一個(gè)額外的好處,那就是當(dāng)生命周期從停止到開始的過渡不太常見,但也不是不可能,它可以重新啟動(dòng)Flow Collect。
這使得執(zhí)行Fragment導(dǎo)航或其他對(duì)生命周期敏感的處理等操作變得安全,而不必?fù)?dān)心生命周期的狀態(tài)是什么。Flow只在安全的生命周期狀態(tài)下被Collect!
Pulling It All Together
把所有的東西放在一起,這就是我用來定義 "單一現(xiàn)場(chǎng)事件 "流的基本模式,以及我如何安全地觀察它。
總結(jié)一下:視圖模型的事件流是用一個(gè)通道接收作為流來定義的。這允許視圖模型提交事件而不必知道觀察者的狀態(tài)。在沒有觀察者的情況下,事件被緩沖了。
視圖(即Fragment或Activity)只有在生命周期達(dá)到開始狀態(tài)后才觀察該流。當(dāng)生命周期到達(dá)停止的事件時(shí),觀察就被取消了。這允許安全地處理事件,而不用擔(dān)心Android生命周期帶來的困難。
最后,在FlowObserver的幫助下,模板被消除了。
你可以在這里看到整個(gè)代碼。
class MainViewModel : ViewModel() {
sealed class Event {
object NavigateToSettings: Event()
data class ShowSnackBar(val text: String): Event()
data class ShowToast(val text: String): Event()
}
private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()
init {
viewModelScope.launch {
eventChannel.send(Event.ShowSnackBar("Sample"))
eventChannel.send(Event.ShowToast("Toast"))
}
}
fun settingsButtonClicked() {
viewModelScope.launch {
eventChannel.send(Event.NavigateToSettings)
}
}
}
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private val viewModel by viewModels<MainViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Note that I've chosen to observe in the tighter view lifecycle here.
// This will potentially recreate an observer and cancel it as the
// fragment goes from onViewCreated through to onDestroyView and possibly
// back to onViewCreated. You may wish to use the "main" lifecycle owner
// instead. If that is the case you'll need to observe in onCreate with the
// correct lifecycle.
viewModel.eventsFlow
.onEach {
when (it) {
MainViewModel.Event.NavigateToSettings -> {}
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
.observeInLifecycle(viewLifecycleOwner)
}
}
我想對(duì)本文中提到的所有作者大加贊賞。他們對(duì)社區(qū)的貢獻(xiàn)大大提高了我工作的質(zhì)量。
Errata
2021年3月編輯
距離我發(fā)表這篇文章已經(jīng)有幾個(gè)月了。谷歌已經(jīng)提供了新的工具(仍處于alpha狀態(tài)),提供了與我下面寫的類似的解決方案。你可以在這里閱讀它。
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
2021年10月編輯
隨著androidx.lifecycle更新到2.4版本,你現(xiàn)在可以使用flowWithLifecycle或repeatWithLifecycle擴(kuò)展函數(shù),而不是我上面定義的那個(gè)。比如說。
viewModel.events
.onEach {
// can get cancelled when the lifecycle state falls below min
}
.flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle, minActiveState = Lifecycle.State.STARTED)
.onEach {
// Do things
}
.launchIn(viewLifecycleOwner.lifecycleScope)
你也可以用 repeatWithLifecycle手動(dòng)做同樣的事情。有一大堆不同的方法可以通過擴(kuò)展函數(shù)使其更易讀。下面是我最喜歡的兩種方法,但也有很多變化。
inline fun <reified T> Flow<T>.observeWithLifecycle(
lifecycleOwner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
noinline action: suspend (T) -> Unit
): Job = lifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
}
inline fun <reified T> Flow<T>.observeWithLifecycle(
fragment: Fragment,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
noinline action: suspend (T) -> Unit
): Job = fragment.viewLifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle, minActiveState).collect(action)
}
還有一個(gè)具有任意最小Activity狀態(tài)的使用例子。
viewModel.events
.observeWithLifecycle(fragment = this, minActiveState = Lifecycle.State.RESUMED) {
// do things
}
viewModel.events
.observeWithLifecycle(lifecycleOwner = viewLifecycleOwner, minActiveState = Lifecycle.State.RESUMED) {
// do things
}
原文鏈接:https://proandroiddev.com/android-singleliveevent-redux-with-kotlin-flow-b755c70bb055
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點(diǎn)擊原文一鍵直達(dá)
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點(diǎn)個(gè)“三連”支持一下??
