Kotlin 协程最近引入了两种Flow类型SharedFlowStateFlow,Android 社​​区开始思考LiveData用其中一种或两种新类型替代的可能性和影响。造成这种情况的两个主要原因是:

  1. LiveData 与 UI 紧密绑定(没有自然的方式将工作卸载到工作线程),并且
  2. LiveData 与Android平台紧密结合。

我们可以从这两个事实得出结论,在 Clean Architecture 术语中,虽然LiveData对于表示层工作正常,但它不适用于领域层,理想情况下应该是独立于平台的(意味着纯 Kotlin/Java 模块);它也不太适合数据层(存储库实现和数据源),因为我们通常应该将数据访问工作卸载到工作线程。

图片来源:中等

不过,我们不能只LiveData用 pure代替Flow。在所有应用层上使用 pureFlow作为LiveData替代品的主要问题是:

  1. Flow是无状态的(无.value访问权限)。
  2. Flow是声明(冷):一个流生成器仅仅描述流量什么,它是唯一的物化收集时。但是,每个收集器Flow都会有效地运行(具体化)一个新的,这意味着上游(昂贵的)数据库访问对于每个收集器都是冗余和重复运行的。
  3. Flow本身对 Android 生命周期一无所知,并且不提供在 Android 生命周期状态更改时自动暂停和恢复收集器。

这些不应被视为纯粹的Flow内在缺陷:这些只是使其不适合作为LiveData替代品的特性,但在其他情况下可能会很强大。

对于(3),我们已经可以使用LifecycleCoroutineScope扩展,例如launchWhenStarted启动协程来收集我们的流——这些收集器将与组件的生命周期同步自动暂停和恢复。

注意:在本文中,我们使用收集和观察作为同义词。收集是 Kotlin Flows 的首选术语(我们收集 a Flow),观察是 Android 的 LiveData 的首选术语(我们观察 a LiveData)。

但是(1)——访问当前状态,以及(2)——对N >= 1收集器只实现一次,对收集器去实体化0呢?

现在,SharedFlowStateFlow为这两个问题提供解决方案。

一个实际的例子

让我们用一个实际用例来举例说明。我们的用例是获取附近的位置。我们假设 Firebase 实时数据库与 GeoFire 库一起使用,它允许查询附近的位置。

使用 LiveData 端到端

图片来源:Medium

让我们首先展示LiveData从数据源一直到我们的视图的使用。数据源负责通过GeoQuery. 当我们收到 a onGeoQueryReady()or 时onGeoQueryError(),我们会LiveData使用自上次 以来进入、退出或移动的位置的聚合来更新该值onGeoQueryReady()。https://proandroiddev.com/media/3213da0e1990ade5c82b12ce54fd7d61

我们的 Repository、ViewModel 和 Activity 应该像这样简单:https://proandroiddev.com/media/ffe052d312d6d3906a69b5dda5d0b266https://proandroiddev.com/media/561151f7c82f825cbaaaaa2d0e00ce56https://proandroiddev.com/media/73cf41190092ac186b5cbf55d80c59da

这种方法可能工作正常,直到您决定使包含存储库接口的域层独立于平台(应该如此)。此外,一旦您需要将工作卸载到数据源上的工作线程,您就会发现使用LiveData.

在数据源和存储库上使用流

图片来源:Medium

让我们转换我们的数据源以使用Flow. 我们有一个流构建器callbackFlow {}将回调转换为冷Flow。当它Flow被收集时,它运行传递给流构建器的代码块,添加GeoQuery侦听器并到达awaitClose {},在那里它挂起直到Flow关闭(即,直到没有人正在收集,或者直到它因任何未捕获的异常而被取消)。当关闭时,它会移除侦听器,并且流被非物质化。https://proandroiddev.com/media/e230722584a13525c0893a840dc14b3d

我们的 Repository 和 ViewModel 保证不做任何更改,但我们的 Activity 现在接收的是 aFlow而不是 a LiveData,因此它需要进行调整:我们将收集.而不是观察LiveDataFlowhttps://proandroiddev.com/media/8892511b7de7108fe8264dd58d40a464

我们使用launchWhenStarted {}收集Flow所以协程只有在Activity达到onStart()生命周期状态时才会自动启动,并在达到onStop()生命周期状态时自动暂停。这类似于自动处理LifecycleLiveData给了我们。

注意:您可以选择LiveData在您的表示层(活动)中继续使用。在这种情况下,您可以使用扩展功能轻松地将 from 转换FlowLiveDatain 。我们将在下一次会议中讨论这个决定的后果,我们将展示使用和端到端更通用,可能更适合您的架构。ViewModelFlow<T>.asLiveData()SharedFlowStateFlow

图片来源:Medium

在视图层使用 Flow 有什么问题?

这种方法的第一个问题是 的处理Lifecycle,它LiveData会自动为我们处理。我们通过使用launchWhenStarted {}上面示例中的实现了类似的行为。

但是还有另一个问题:因为它Flow是声明性的并且仅在收集时运行(具体化),如果我们有多个收集器,则将为每个收集器运行一个新的流程,彼此完全独立。根据完成的操作,例如数据库或网络操作,这可能非常低效。如果我们期望操作只执行一次以确保正确性,它甚至可能导致错误状态。在我们的实际示例中,我们将为GeoQuery每个收集器添加一个新的侦听器——可能不是一个关键问题,但肯定会浪费内存和 CPU 周期。

注:如果您将您的信息库FlowLiveData使用的,则成为了唯一的收藏家,而且不管如何在表示层许多观察家来说,只有一个会被收集。然而,对于建筑很好地工作,你需要保证每一个你的其他组件访问您从,从不直接从。这可以证明自己是一个挑战,具体取决于您的应用程序的解耦程度:所有需要存储库的组件,例如交互器(用例)实现,现在都将依赖于实例来获取实例,并且这些组件的范围需要相应地加以限制。Flow<T>.asLiveData()ViewModelLiveDataFlowFlowLiveDataViewModelFlowRepositoryActivityViewModel

我们只需要一个GeoQuery监听器,不管我们在视图层有多少个收集器。我们可以通过在所有收集器之间共享流程来实现这一点。

SharedFlow 来救援

SharedFlow是一个Flow允许在多个收集器之间共享自身,因此对于所有同时收集器,只有一个流有效地运行(具体化)。如果定义了一个SharedFlow访问数据库的,并且它被多个收集器收集,那么数据库访问只会运行一次,结果数据将共享给所有收集器。

StateFlow也可以用来实现相同的行为:它是一个专门SharedFlow.value(它的当前状态)和特定的SharedFlow配置(约束)。我们稍后会讨论这些限制。

我们有一个将 anyFlow转换为 a的运算符SharedFlow

fun <T> Flow < T >.shareIn( 
scope : CoroutineScope ,
started : SharingStarted ,
replay : Int = 0
): SharedFlow < T > (source)

让我们将其应用于我们的数据源。

scope是实现Flow意志的所有计算的地方。由于我们的数据源是 a @Singleton,我们可以使用应用程序 process’ LifecycleScope,它是LifecycleCoroutineScope在进程创建时创建的,并且仅在进程销毁时销毁。

对于started参数,我们可以使用SharingStarted.WhileSubscribed(),这使得我们Flow开始共享(物化),只有当用户圈从数量01,并停止共享时,用户圈从数量10。这类似于LiveData我们之前通过GeoQueryonActive()回调中添加侦听器并在回调中移除侦听器来实现的行为onInactive()。我们还可以将其配置为急切启动(立即物化且永不取消实现)或延迟启动(首次收集时实现,永不取消),但我们确实希望它在下游未收集时停止上游数据库收集。

术语注意事项:正如我们对 LiveData 使用术语观察者和对冷流使用收集器一样,我们对 SharedFlow 使用术语订阅者。

对于replay参数,我们可以使用1:新订阅者将在订阅后立即获得最后发出的值。https://proandroiddev.com/media/a891a3ab2a4e3cc7b149c24db95aa4ba

将 aSharedFlow视为流收集器本身可能会有所帮助,它将上游的冷流具体化为热流,并在下游的许多收集器之间共享收集的值。一个人在冷上游流和多个下游收集器之间。

现在,我们可能会认为我们Activity不需要调整。错误的!有一个问题:在启动的协程中收集流时 launchWhenStarted {} ,协程将是 暂停 在 onStop() 和 恢复 在 onStart() , 但 它仍然会订阅流. 为了MutableSharedFlow<T>, 它的意思是 MutableSharedFlow<T>.subscriptionCount对于暂停的协程不会改变。为了利用权力SharingStarted.WhileSubscribed() ,我们需要实际取消订阅 onStop() ,然后再次订阅 onStart(). 这意味着取消收集协程并重新创建它。

(有关更多详细信息,请参阅此问题此问题)。

让我们为这个通用目的创建一个类:https://proandroiddev.com/media/5a17d405b461804f757d9758d18822ab

注意:如果你想在你的项目中使用这个自定义观察者,你可以使用这个库:https : //github.com/psteiger/flow-lifecycle-observer

现在,我们可以调整我们Activity的使用.observeIn(LifecycleOwner)我们刚刚创建的扩展函数:https://proandroiddev.com/media/c9d75864f6107a7cd7dac144b41fbb2f

与创建收集器协同程序observeIn(LifecycleOwner)时,将被破坏LifecycleOwnerLifecycle河段CREATED状态(前右onStop()一旦到达调用)和将重新STARTED状态(后onStart()调用)。

注意:为什么要CREATED声明?不应该是STOPPED国家吗?乍一看,这听起来违反直觉,但它完全有道理。Lifecycle.State只有以下状态:CREATEDDESTROYEDINITIALIZEDRESUMEDSTARTED没有STOPPEDPAUSED状态。当生命周期到达 时onPause(),它不会转到新状态,而是返回到该STARTED状态。当它到达时onStop(),它返回CREATED状态。

来源:android.com

我们现在有一个实现一次的数据源,但将其数据共享给所有订阅者。一旦没有订阅者,其上游收集将立即停止,并在第一个订阅者重新出现时重新启动。它不依赖于 Android 平台,也不依赖于主线程(Flow只需应用.flowOn()运算符:flowOn(Dispatchers.IO)或,就可以在其他线程中进行转换.flowOn(Dispatchers.Default))。

但是如果我需要最终访问流的当前状态而不收集它怎么办?

如果我们真的需要像Flow使用 with.value那样访问状态LiveData,我们可以使用StateFlow,这是一个专门的、受限制的SharedFlow.

代替将的shareIn()运营商兑现的流动,我们可以应用stateIn()

fun <T> Flow < T >.stateIn( 
scope : CoroutineScope ,
started : SharingStarted ,
initialValue : T
): StateFlow < T > (source)

从methods参数可以看出,sharedIn()和之间有两个基本区别stateIn()

  1. stateIn()不支持replay自定义。StateFlowSharedFlow一个固定的replay=1。这意味着新订阅者将在订阅后立即获得当前状态。
  2. stateIn()需要一个初始值。这意味着如果您当时没有初始值,您将需要使StateFlow<T>类型可以为T空,或者使用 asealed class来表示一个空的初始值。

文档

状态流是共享流

状态流是SharedFlow 的一种特殊用途、高性能和高效的实现,适用于共享状态的狭窄但广泛使用的情况。有关适用于所有共享流的基本规则、约束和运算符,请参阅SharedFlow文档。

状态流始终有一个初始值,向新订阅者重放一个最近的值,不再缓冲任何值,但保留最后发出的值,并且不支持resetReplayCache。使用以下参数创建状态流并对其应用distinctUntilChanged运算符时,状态流的行为与共享流相同:

// MutableStateFlow(initialValue) 是具有以下参数的共享流:
val shared = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
shared.tryEmit(initialValue) // 发出初始值
val state = shared.distinctUntilChanged() // 获取类似 StateFlow 的行为

当您需要StateFlow对其行为进行调整时,请使用SharedFlow,例如额外缓冲、重放更多值或省略初始值。

但是,请注意选择时明显的妥协SharedFlow:您将失去StateFlow<T>.value

选择哪个,StateFlow 还是 SharedFlow?

回答这个问题的简单方法是尝试回答其他几个问题:

“我真的需要在任何给定时间使用 myFlow.value 访问流的当前状态吗?”

如果这个问题的答案是否定的,你可以考虑SharedFlow

“我需要支持发出和收集重复值吗?”

如果这个问题的答案是肯定的,您 需要 SharedFlow.

“我是否需要为新订阅者重播超过最新值的内容?”

如果这个问题的答案是肯定的,您 需要 SharedFlow.

正如我们所看到的,StateFlow因为一切都不会自动成为正确的答案。

1. 它忽略(混淆)重复值,这是不可配置的。有时您需要不要忽略重复的值,例如:将尝试结果存储在流中的连接尝试,并且需要在每次失败后重试。

2.另外,它需要一个初始值。因为SharedFlow没有.value,所以不需要用初始值实例化——收集器只会挂起直到第一个值出现,.value在任何值到达之前没有人会尝试访问。如果您没有初始值,StateFlow则必须使该StateFlow类型T?可以null为空并用作初始值(或sealed class为默认无值声明 a )。

3. 此外,您可能想要调整该replay值。SharedFlow可以重播n新订阅者的最后一个值。StateFlow 有一个固定replay1——它只共享当前状态值。

两者都支持SharingStartedEagerly,LazilyWhileSubscribed()) 配置。我通常使用SharingStarted.WhileSubscribed()并销毁/重建所有我的收藏Activity onStart()onStop(),所以数据源,当用户不使用该应用收集的上游将停止(这是类似于删除/重新添加对听众LiveData onActive()onInactive()

StateFlow强加于的约束SharedFlow可能不是最适合您的,您可能想要调整行为并选择使用SharedFlow. 就我个人而言,我很少需要访问myFlow.value,我喜欢它SharedFlow的灵活性,所以我通常选择SharedFlow.

在官方文档中阅读有关StateFlowSharedFlow 的更多信息。

0
分类: 未分类

bayshier

愿世间每个美好的灵魂都能被温柔以待

0 条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注