【译】Android SingleLiveEvent Redux with Kotlin Flow

1,126 阅读11分钟

这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。

从这篇文章大家可以了解到我们在使用LiveData和Flow时,是如何一步步发现问题,并解决问题的,特别是站在设计者的角度来看这些问题,你会学到解决问题的一般方法。

自从Jose Alcérreca发表了他的文章 "SingleLiveEvent Case "以来,已经过去了好几年。这篇文章对许多开发者来说是一个很好的起点,因为它让他们思考ViewModels和相关视图(无论是Fragment还是Activity)之间的不同通信模式。

这篇文章可以看这里。medium.com/androiddeve…

对于SingleLiveEvent案例,已经有许多关于如何改进该模式的回应。我最喜欢的一篇文章是由 Hadi Lashkari Ghouchani的文章proandroiddev.com/livedata-wi…

然而,上面提到的两种情况仍然使用LiveData作为备选的数据Store。我觉得仍有改进的余地,尤其是在使用Kotlin的coroutines和flow时。在这篇文章中,我将描述我如何处理一次性事件,以及如何在Android生命周期中安全地观察这些事件。

Background

为了与其他关于SingleLiveEvent的文章,或者说使用该模式的变体文章保持一致,我将把事件定义为采取一次、且仅一次行动的通知。最初的SingleLiveEvent文章以显示SnackBar为例,但你也可以把其他一次性动作,如Fragment导航、启动Activity、显示通知等作为「事件」的例子。

在MVVM模式中,ViewModel和它相关的视图(Fragment或Activity)之间的通信通常是通过遵循观察者模式来完成的。这使得视图模型与视图解耦,允许视图经历各种生命周期状态,而不需要向观察者发送数据。

在我的ViewModels中,我通常会公开两个流来进行观察。第一个是视图状态。这个数据流定义了用户界面的状态。它可以被反复观察,并且通常由Kotlin StateFlow、LiveData或其他类型的数据存储来支持,暴露出一个单一的值。但是我将会忽略这个流程,因为它不是本文的重点。然而,如果你感兴趣的话,有很多文章描述了如何用StateFlow或LiveData实现UI状态。

第二个可观察流,也是本文的重点,要有趣得多。这个数据流的目的是通知视图执行一个动作,而且只有一次。比如说,导航到另一个Fragment。让我们探讨一下这个流程有哪些需要注意的地方。

Requirements

可以说,事件是重要的,甚至是关键的。所以让我们为这个流程和它的观察者定义一些要求。

  • 新事件不能覆盖未观察到的事件。
  • 如果没有观察者,事件必须缓冲到观察者开始消费它们。
  • 视图可能有重要的生命周期状态,在此期间它只能安全地观察事件。因此,观察者可能并不总是在某个特定的时间点上Activity或消费流。

A Safe Emitter of Events

因此,满足第一个要求,很明显,一个流是必要的。LiveData或任何conflates Kotlin flow,如StateFlow或ConflatedBroadcastChannel,都不合适。一组快速发射的事件可能会相互覆盖,而只有最后一个事件被发射到观察者那里。

那么使用SharedFlow呢?这能帮助吗?不幸的是,不能。SharedFlow是热的。这意味着在没有观察者的时期,比如说在配置改变的时候,发射到流中的事件会被简单地丢弃。遗憾的是,这也使得SharedFlow不适合发射事件。

那么,我们有什么办法来满足第二和第三个要求呢?幸运的是,一些文章已经为我们描述过了。

JetBrains的Roman Elizarov写了一篇关于各种类型流量的不同使用情况的文章。

这篇文章中特别有趣的是 "A use-case for channels "一节,他描述了我们所需要的东西——一个单次事件总线,是一个缓冲的事件流。文章地址如下:elizarov.medium.com/shared-flow…

......channels也有其应用场合。channels被用来处理那些必须被精确处理一次的事件。这发生在一个设计中,有一种类型的事件通常有一个订阅者,但间歇性地(在启动或某种重新配置期间)根本没有订阅者,而且有一个要求,即所有发布的事件必须保留到一个订阅者出现。

现在我们已经找到了一种安全的方法来发射事件,让我们用一些示例事件来定义一个ViewModel的基本结构。

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)
        }
    }
}

上面的例子中,视图模型在构建时立即发射了两个事件。观察者可能不会马上消费它们,所以它们被简单地缓冲,并在观察者开始从Flow中collect时被发射出来。在上面的例子中,还包括了视图模型对按钮点击的处理。

事件发射器的实际定义出乎意料的简单和直接。现在,事件的发射方式已经定义好了,让我们继续讨论如何在Android的背景下安全地观察这些事件,以及不同的生命周期状态带来的限制。

A Safe Observer of Events

Android Framework强加给开发者的不同的生命周期可能很难处理。许多操作只能在某些生命周期状态下安全地执行。例如,Fragment导航只能在onStart之后、onStop之前进行。

那么,我们如何安全地观察只在给定生命周期状态下的事件流呢?如果我们观察视图模型的事件流,比如说一个Fragment,在Fragment提供的coroutine范围内,这是否能满足我们的需要?

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的文档指出,当生命周期被销毁时,这个Scope会被取消。这意味着有可能在生命周期达到停止状态但尚未销毁的情况下收到事件。如果在处理事件的过程中执行诸如Fragment导航之类的操作,这可能会有问题。

使用launchWhenX的误区

也许我们可以用launchWhenStarted来控制一个事件被接收的不同生命周期状态?比如说。

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写了一篇关于Android生命周期Coroutines的精彩深度文章,他描述了launchWhenX这组函数背后的基本机制。他在文章中指出。

launchWhenX函数在生命周期离开期望状态时不会被取消。它们只是被暂停了。只有当生命周期达到DESTROYED状态时才会取消。

我对他的文章进行了回应,证明在任何 launchWhenX 函数中观察一个流程时,都有可能在配置改变时丢失事件。这篇回应很长,我就不在这里重复了,所以我鼓励你去读它。

地址如下:themikeferguson.medium.com/pitfalls-of…

关于这个问题的简明演示,见gist.github.com/fergusonm/8…

因此,遗憾的是,我们也不能利用 launchWhenX 的扩展函数来帮助控制一个流在什么生命周期状态下被观察。那么我们能做什么呢?退一步讲,如果我们花点时间看看我们要做什么,我们可以更容易地找出一个解决方案,只在特定的生命周期状态下进行观察。分解这个问题,我们注意到,我们真正想做的是在一个状态下开始观察,在另一个状态下停止观察。

如果我们使用另一个工具,比如RxJava,我们可以在onStart生命周期回调中订阅事件流,并在onStop回调中进行处置。(类似的模式也可以用于通用回调)。

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做到这一点?嗯,我们可以。当生命周期被破坏时,作用域仍然会被取消,但是我们可以将观察者处于Activity状态的时间紧缩到只有启动和停止之间的生命周期状态。

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()
}

这满足了第三个要求,解决了只在安全生命周期状态下观察事件流的问题,但它引入了大量的模板。

Cleaning Things Up

如果我们把管理这项工作的责任委托给其他东西,以帮助消除这些模板,会怎么样?Patrick Steiger的文章《用LiveData替代StateFlow或SharedFlow》中就有一个惊人的小插曲。(这也是一篇很好的读物)。原文地址如下:proandroiddev.com/should-we-c…

他创建了一组扩展函数,当一个生命周期的所有者达到开始时,自动订阅一个流量Collect器,当生命周期达到停止阶段时,取消Collect器。下面是我对其稍加修改的版本。

(2021年10月编辑:请看下面的更新版本,它利用了最近的库变化。)

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, {})

使用这些扩展功能是超级简单和直接的。

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)
}

现在我们有了一个事件观察者,它只在达到开始的生命周期后进行观察,当达到停止的生命周期时,它就取消。

它还有一个额外的好处,那就是当生命周期从停止到开始的过渡不太常见,但也不是不可能,它可以重新启动Flow Collect。

这使得执行Fragment导航或其他对生命周期敏感的处理等操作变得安全,而不必担心生命周期的状态是什么。Flow只在安全的生命周期状态下被Collect!

Pulling It All Together

把所有的东西放在一起,这就是我用来定义 "单一现场事件 "流的基本模式,以及我如何安全地观察它。

总结一下:视图模型的事件流是用一个通道接收作为流来定义的。这允许视图模型提交事件而不必知道观察者的状态。在没有观察者的情况下,事件被缓冲了。

视图(即Fragment或Activity)只有在生命周期达到开始状态后才观察该流。当生命周期到达停止的事件时,观察就被取消了。这允许安全地处理事件,而不用担心Android生命周期带来的困难。

最后,在FlowObserver的帮助下,模板被消除了。

你可以在这里看到整个代码。

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)
    }

}

我想对本文中提到的所有作者大加赞赏。他们对社区的贡献大大提高了我工作的质量。

Errata

2021年3月编辑

距离我发表这篇文章已经有几个月了。谷歌已经提供了新的工具(仍处于alpha状态),提供了与我下面写的类似的解决方案。你可以在这里阅读它。

medium.com/androiddeve…

2021年10月编辑

随着androidx.lifecycle更新到2.4版本,你现在可以使用flowWithLifecycle或repeatWithLifecycle扩展函数,而不是我上面定义的那个。 比如说。

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手动做同样的事情。有一大堆不同的方法可以通过扩展函数使其更易读。下面是我最喜欢的两种方法,但也有很多变化。

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)
}

还有一个具有任意最小Activity状态的使用例子。

viewModel.events
    .observeWithLifecycle(fragment = this, minActiveState = Lifecycle.State.RESUMED) {
        // do things
    }

viewModel.events
    .observeWithLifecycle(lifecycleOwner = viewLifecycleOwner, minActiveState = Lifecycle.State.RESUMED) {
        // do things
    }

原文链接:proandroiddev.com/android-sin…

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问