前言
原标题: Substituting Android’s LiveData: StateFlow or SharedFlow?
原文地址: Substituting Android’s LiveData: StateFlow or SharedFlow?
原文作者:Patrick Steiger
Kotlin Coroutines最近推出了两种Flow类型,SharedFlow和StateFlow,Android社区开始考虑使用用这些新类型去替代LiveData的可能性。
这样做的两个主要原因是:
- 1.LiveData与UI紧密绑定(没有自然的方式将工作卸载到工作线程)
- 2.LiveData与Android平台紧密绑定。
我们可以从这两个事实得出结论,就Clean Architecture而言,尽管LiveData在展示层上运行良好,但它并不能很好地集成到域层中,后者应该独立于平台(意味着一个纯粹的Kotlin / Java模块); 而且它也不太适合数据层(存储库实现和数据源),因为我们通常应该将数据访问工作分担给工作线程。
但是,我们不能仅用纯Flow代替LiveData。使用纯Flow作为LiveData替代品的主要问题是:
- 1.Flow是无状态的(并且不能通过.value访问)。
- 2.Flow是声明性的,一个Flow Builder仅描述Flow是什么,并且仅在收集时才具体化。 并且将为每个收集器有效地实例化新的Flow,这意味着将为每个收集器冗余且重复地运行上游昂贵的数据库访问。
- 3.Flow本身对Android生命周期一无所知,并且不会在Android生命周期状态发生变化时自动暂停和恢复收集器。
这些不应被视为Flow固有缺陷:这些只是使其无法很好地替代LiveData,而在其他情况下却可能很实用。
对于(3),我们已经可以使用LifecycleCoroutineScope扩展(例如launchWhenStarted)来启动协程以收集我们的flows-这些收集器将自动暂停并与组件的Lifecycle同步恢复。
注意:在本文中,我们将收集和观察用作同义词概念。 收集是Kotlin Flows(我们收集一个Flow)的首选术语,观察是Android LiveData(我们观察一个LiveData)的首选术语。
但是关于(1)访问当前状态,(2)对于N> = 1个收集器仅实现一次,而对于0个收集器则消失,我们该如何实现呢?
现在,SharedFlow和StateFlow为这两个问题提供了解决方案。
我们举一个例子。我们的用例正在获取附近的位置。我们假设将Firebase实时数据库与GeoFire库一起使用,该库允许查询附近的位置。
使用LiveData端到端
让我们首先展示从数据源一直到视图的LiveData用法。
数据源负责通过GeoQuery连接到Firebase实时数据库。
当我们收到onGeoQueryReady或onGeoQueryError时,将使用自上一个onGeoQueryReady以来输入,退出或移动的位置的总和来更新LiveData值。
@Singleton
class NearbyUsersDataSource @Inject constructor() {
// Ideally, those should be constructor-injected.
val geoFire = GeoFire(FirebaseDatabase.getInstance().getReference("geofire"))
val geoLocation = GeoLocation(0.0, 0.0)
val radius = 100.0
val geoQuery = geoFire.queryAtLocation(geoLocation, radius)
// Listener for receiving GeoLocations
val listener: GeoQueryEventListener = object : GeoQueryEventListener {
val map = mutableMapOf<Key, GeoLocation>()
override fun onKeyEntered(key: String, location: GeoLocation) {
map[key] = location
}
override fun onKeyExited(key: String) {
map.remove(key)
}
override fun onKeyMoved(key: String, location: GeoLocation) {
map[key] = location
}
override fun onGeoQueryReady() {
_locations.value = State.Ready(map.toMap())
}
override fun onGeoQueryError(e: DatabaseError) {
_locations.value = State.Error(map.toMap(), e.toException())
}
}
// Listen for changes only while observed
private val _locations = object : MutableLiveData<State>() {
override fun onActive() {
geoQuery.addGeoQueryEventListener(listener)
}
override fun onInactive() {
geoQuery.removeGeoQueryEventListener(listener)
}
}
// Expose read-only LiveData
val locations: LiveData<State> by this::_locations
sealed class State(open val value: Map<Key, GeoLocation>) {
data class Ready(
override val value: Map<Key, GeoLocation>
) : State(value)
data class Error(
override val value: Map<Key, GeoLocation>,
val exception: Exception
) : State(value)
}
}
然后,我们的Repository,ViewModel和Activity应该很简单:
@Singleton
class NearbyUsersRepository @Inject constructor(
nearbyUsersDataSource: NearbyUsersDataSource
) {
val locations get() = nearbyUsersDataSource.locations
}
class NearbyUsersViewModel @ViewModelInject constructor(
nearbyUsersRepository: NearbyUsersRepository
) : ViewModel() {
val locations get() = nearbyUsersRepository.locations
}
@AndroidEntryPoint
class NearbyUsersActivity : AppCompatActivity() {
private val viewModel: NearbyUsersViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.locations.observe(this) { state: State ->
// Update views with the data.
}
}
}
在您决定使包含存储库接口的域层(独立于平台)之前,这种尝试可能不错。 同样,一旦需要将工作分担到数据源上的工作线程上,您将发现LiveData没有简单,惯用的方法。
在数据源和Repository上使用Flow
让我们将数据源转换为使用Flow。 我们有一个Flow生成器callbackFlow,它将回调转换为Flow。
收集此Flow后,它将运行传递给Flow构建器的代码块,添加GeoQuery侦听器并到达awaitClose,在该处暂停直到Flow被关闭(即直到没有人收集或因为异常被取消为止).
关闭时,它将删除监听器,并且Flow将取消实现。
@Singleton
class NearbyUsersDataSource @Inject constructor() {
// Ideally, those should be constructor-injected.
val geoFire = GeoFire(FirebaseDatabase.getInstance().getReference("geofire"))
val geoLocation = GeoLocation(0.0, 0.0)
val radius = 100.0
val geoQuery = geoFire.queryAtLocation(geoLocation, radius)
private fun GeoQuery.asFlow() = callbackFlow {
val listener: GeoQueryEventListener = object : GeoQueryEventListener {
val map = mutableMapOf<Key, GeoLocation>()
override fun onKeyEntered(key: String, location: GeoLocation) {
map[key] = location
}
override fun onKeyExited(key: String) {
map.remove(key)
}
override fun onKeyMoved(key: String, location: GeoLocation) {
map[key] = location
}
override fun onGeoQueryReady() {
emit(State.Ready(locations.toMap()))
}
override fun onGeoQueryError(e: DatabaseError) {
emit(State.Error(map.toMap(), e.toException()))
}
}
addGeoQueryEventListener(listener)
awaitClose { removeGeoQueryEventListener(listener) }
}
val locations: Flow<State> = geoQuery.asFlow()
sealed class State(open val value: Map<Key, GeoLocation>) {
data class Ready(
override val value: Map<Key, GeoLocation>
) : State(value)
data class Error(
override val value: Map<Key, GeoLocation>,
val exception: Exception
) : State(value)
}
}
我们的Repository和ViewModel不做任何更改,但是我们的Activity现在接收到Flow而不是LiveData,因此它需要进行调整:我们将收集Flow而不是观察LiveData。
@AndroidEntryPoint
class NearbyUsersActivity : AppCompatActivity() {
private val viewModel: NearbyUsersViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launchWhenStarted {
viewModel.locations.collect {
// Update views with the data.
}
}
}
}
我们使用launchWhenStarted {}来收集Flow,因此协程仅在Activity达到onStart()生命周期状态时才自动启动,而当它达到onStop()生命周期状态时将自动暂停。 这类似于LiveData给我们的生命周期自动处理。
注意:您可以选择在展示层中继续使用LiveData。 在这种情况下,您可以使用Flow .asLiveData扩展函数轻松地在ViewModel中将Flow从Flow转换为LiveData。 这个决定将带来后果,我们将继续进行讨论,并且将证明端到端使用SharedFlow和StateFlow更加通用,并且可能更适合您的体系结构。
在View层中使用Flow有什么问题?
这种方法的第一个问题是生命周期的处理,LiveData会自动为我们处理。 在上面的示例中,我们通过使用launchWhenStarted {}实现了类似的行为。
但是还有另一个问题:由于Flow是声明性的,并且仅在收集时运行(实例化),因此,如果我们有多个收集器,则将为每个收集器运行一个新的Flow,彼此完全独立。
根据完成的操作(例如数据库或网络操作),这可能会非常无效。
如果我们期望操作只进行一次以确保正确性,则甚至可能导致错误的状态。
在我们的实际示例中,我们将为每个收集器添加一个新的GeoQuery监听器-可能不是关键问题,但肯定会浪费内存和CPU周期。
注意:如果通过在ViewModel中使用Flow .asLiveData()将存储库流转换为LiveData,则LiveData将成为Flow的唯一收集器,无论展示层中有多少观察者,都将只有一个Flow 集。 但是,为了使该架构正常工作,您需要确保自己的所有其他组件都可以从ViewModel访问LiveData,而绝不能直接从存储库访问Flow。 这可能是一个挑战,具体取决于应用程序的分离程度:所有需要存储库的组件现在都将依赖Activity实例来获取ViewModel实例,以及这些组件的范围 需要相应地加以限制。
无论我们在View层中有多少个收集器,我们都只需要一个GeoQuery监听器。 我们可以通过共享所有收集器之间的流来实现此目的。
SharedFlow
SharedFlow是一种流,它允许在多个收集器之间共享自己,因此对于所有同时收集器,只有一个流有效地运行(实现)。
如果定义访问数据库的SharedFlow并且由多个收集器收集,则数据库访问将仅运行一次,并且所得到的数据将共享给所有收集器。
StateFlow也可以用于实现相同的行为:它是具有.value(其当前状态)和特定SharedFlow配置(约束)的专用SharedFlow。 稍后我们将讨论这些限制。
我们有一个运算符,用于将任何Flow转换为SharedFlow
fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T> (source)
让我们将其应用于我们的数据源。
scope是完成实现该流程的所有计算的地方。
由于我们的数据源是@Singleton,因此我们可以使用应用程序进程的LifecycleScope,它是一个LifecycleCoroutineScope,它是在进程创建时创建的,仅在进程销毁时才销毁。
对于started参数,我们可以使用SharingStarted.WhileSubscribed(),这使Flow仅在订阅者数量从0变为1时才开始共享(实现),并在订阅者数量从1变为0时停止共享。 与我们之前通过在onActive()回调中添加GeoQuery侦听器并在onInactive()回调中删除侦听器实现的LiveData行为类似。 我们还可以将其配置为立即启动(立即实现,而不再取消实现)或懒启动(在首次收集时实现,而从未取消实现),但是我们希望它在下游不再收集时停止上游数据库的收集。
关于术语的注意事项:正如我们将术语“观察者”用于LiveData,将“收集器”用于Flow一样,我们将术语“订阅”用于SharedFlow。
对于replay参数,我们可以设置为1,新订阅者将在订阅时立即获得最后发出的值。
@Singleton
class NearbyUsersDataSource @Inject constructor() {
// Ideally, those should be constructor-injected.
val geoFire = GeoFire(FirebaseDatabase.getInstance().getReference("geofire"))
val geoLocation = GeoLocation(0.0, 0.0)
val radius = 100.0
val geoQuery = geoFire.queryAtLocation(geoLocation, radius)
private fun GeoQuery.asFlow() = callbackFlow {
val listener: GeoQueryEventListener = object : GeoQueryEventListener {
val map = mutableMapOf<Key, GeoLocation>()
override fun onKeyEntered(key: String, location: GeoLocation) {
map[key] = location
}
override fun onKeyExited(key: String) {
map.remove(key)
}
override fun onKeyMoved(key: String, location: GeoLocation) {
map[key] = location
}
override fun onGeoQueryReady() {
emit(State.Ready(map.toMap())
}
override fun onGeoQueryError(e: DatabaseError) {
emit(State.Error(map.toMap(), e.toException())
}
}
addGeoQueryEventListener(listener)
awaitClose { removeGeoQueryEventListener(listener) }
}.shareIn(
ProcessLifecycleOwner.get().lifecycleScope,
SharingStarted.WhileSubscribed(),
1
)
val locations: Flow<State> = geoQuery.asFlow()
sealed class State(open val value: Map<Key, GeoLocation>) {
data class Ready(
override val value: Map<Key, GeoLocation>
) : State(value)
data class Error(
override val value: Map<Key, GeoLocation>,
val exception: Exception
) : State(value)
}
}
将SharedFlow本身视为Flow收集器可能会有所帮助,它会将上游的冷流变为热流,并在下游的许多收集器之间共享收集的值。 在冷的上游水流和多个下游收集器之间的中间位置有一个人。
现在,我们可能会以为Activity不需要调整。
错误! 有一个陷阱:在使用launchWhenStarted {}启动的协程中收集流时,协程将在onStop()上暂停,并在onStart()上恢复,但仍将订阅该流。
对于MutableSharedFlow ,这意味着MutableSharedFlow .subscriptionCount对于暂停的协程不会更改。 为了利用SharingStarted.WhileSubscribed()的功能,我们实际上需要在onStop()上退订,然后在onStart()上再次订阅。 这意味着取消收集协程并重新创建它。
让我们为此目的创建一个类:
@PublishedApi
internal class ObserverImpl<T> (
lifecycleOwner: LifecycleOwner,
private val flow: Flow<T>,
private val collector: suspend (T) -> Unit
) : DefaultLifecycleObserver {
private var job: Job? = null
override fun onStart(owner: LifecycleOwner) {
job = owner.lifecycleScope.launch {
flow.collect {
collector(it)
}
}
}
override fun onStop(owner: LifecycleOwner) {
job?.cancel()
job = null
}
init {
lifecycleOwner.lifecycle.addObserver(this)
}
}
inline fun <reified T> Flow<T>.observe(
lifecycleOwner: LifecycleOwner,
noinline collector: suspend (T) -> Unit
) {
ObserverImpl(lifecycleOwner, this, collector)
}
inline fun <reified T> Flow<T>.observeIn(
lifecycleOwner: LifecycleOwner
) {
ObserverImpl(lifecycleOwner, this, {})
}
现在,我们可以调整Activity以使用刚刚创建的.observeIn(LifecycleOwner)扩展功能:
@AndroidEntryPoint
class NearbyUsersActivity : AppCompatActivity() {
private val viewModel: NearbyUsersViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
viewModel
.locations
.onEach { /* new locations received */ }
.observeIn(this)
}
}
当LifecycleOwner的生命周期达到CREATED状态(恰好在onStop()调用之前)时,使用observeIn(LifecycleOwner)创建的收集器协程将被销毁,并且一旦达到STARTED状态(在onStart()调用之后)将被重新创建。
注意:为什么是CREATED状态? 不应该是STOPED状态吗? 乍一看听起来有点违反直觉,但确实很合理。 Lifecycle.State仅具有以下状态:CREATED, DESTROYED, INITIALIZED, RESUMED, STARTED。 没有STOPPED和PAUSED状态。 当生命周期达到onPause()时,它不会返回新状态,而是返回到STARTED状态。 当到达onStop()时,它返回到CREATED状态。
现在,我们有了一个数据源,该数据源一次实现,但将其数据共享给所有订阅者。
一旦没有订阅者,它的上游收集将停止,并在第一个订阅者重新出现时重新启动。
它不依赖于Android平台,也不与主线程绑定(仅通过应用.flowOn()运算符即可在其他线程中进行流转换:
flowOn(Dispatchers.IO)或.flowOn(Dispatchers.Default) )。
但是,如果我最终需要访问Flow的当前状态而不收集它怎么办?
如果确实需要像使用LiveData一样使用.value访问Flow的状态,则我们可以使用StateFlow,它是一种专用的受限SharedFlow。
ShareFlow使用shareIn来实现 StateFlow使用stateIn实现
fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T> (source)
从方法参数可以看出,sharedIn()和stateIn()之间有两个基本区别:
1.stateIn()不支持replay自定义。 StateFlow是具有固定replay=1的SharedFlow。 这意味着新订阅者将在订阅后立即获得当前状态。
2.stateIn()需要一个初始值。 这意味着如果您当时没有初始值,则需要使StateFlow 类型T为可为空,或使用密封类来表示空的初始值。
选择哪个,StateFlow或SharedFlow?
回答此问题的简单方法是尝试回答其他一些问题:
“我真的需要在任何给定时间使用myFlow.value访问Flow的当前状态吗?”
如果此问题的答案为“否”,则可以考虑使用SharedFlow。
“我需要支持发出和收集重复值吗?”
如果对这个问题的回答是“是”,则需要SharedFlow。
“对于新订户,我是否需要重播超过最新值的内容?”
如果对这个问题的回答是“是”,则需要SharedFlow。
正如我们所看到的,对于所有内容,StateFlow并不是自动的正确答案。
1.它忽略(合并)重复的值,并且这是不可配置的。 有时您不需要忽略重复的值,例如:连接尝试将尝试的结果存储在流中,并且每次失败后都需要重试。
2.另外,它需要一个初始值。 由于SharedFlow没有.value,因此不需要使用初始值实例化-收集器将暂停直到第一个值出现,并且没有人会尝试在任何值到达之前访问.value。 如果您没有StateFlow的初始值,则必须将StateFlow类型设为可为null的T? 并使用null作为初始值(或使用密封类来表示空的初始值)。
3.另外,您可能需要调整重播值。 SharedFlow可以为新订户重播最后n个值。 StateFlow的固定重播值为1-它仅共享当前状态值
两者都支持SharingStarted(立刻,懒加载或WhileSubscribed())配置。我通常使用SharingStarted.WhileSubscribed()
并在Activity onStart()/ onStop()时,销毁/重新创建所有收集器,
因此,当用户不积极使用该应用程序时,数据源上游收集将停止
StateFlow施加在SharedFlow上的约束可能不是最适合您,您可能需要调整行为并选择使用SharedFlow。就个人而言,我很少需要访问myFlow.value,并且享受SharedFlow的灵活性,因此我通常选择SharedFlow。
一个使用SharedFlow的实例
考虑以下围绕Google Billing Client库的包装。我们有一个MutableSharedFlow billingClientStatus,用于存储当前到计费服务的连接状态。
我们将其初始值设置为SERVICE_DISCONNECTED。我们收集billingClientStatus,当它不正常时,我们尝试将startConnection()连接到计费服务。
如果连接尝试失败,我们将发出SERVICE_DISCONNECTED。
在该示例中,如果billingClientStatus是MutableStateFlow而不是MutableSharedFlow,则当其值已经为SERVICE_DISCONNECTED且我们尝试将其设置为相同(连接重试失败)时,它将忽略更新,因此,它将不会尝试重新连接再次。
@Singleton
class Biller @Inject constructor(
@ApplicationContext private val context: Context,
) : PurchasesUpdatedListener, BillingClientStateListener {
private var billingClient: BillingClient =
BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
private val billingClientStatus = MutableSharedFlow<Int>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override fun onBillingSetupFinished(result: BillingResult) {
billingClientStatus.tryEmit(result.responseCode)
}
override fun onBillingServiceDisconnected() {
billingClientStatus.tryEmit(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
}
// ...
// Suspend until billingClientStatus == BillingClient.BillingResponseCode.OK
private suspend fun requireBillingClientSetup(): Boolean =
withTimeoutOrNull(TIMEOUT_MILLIS) {
billingClientStatus.first { it == BillingClient.BillingResponseCode.OK }
true
} ?: false
init {
billingClientStatus.tryEmit(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
billingClientStatus.observe(ProcessLifecycleOwner.get()) {
when (it) {
BillingClient.BillingResponseCode.OK -> with (billingClient) {
updateSkuPrices()
handlePurchases()
}
else -> {
delay(RETRY_MILLIS)
billingClient.startConnection(this@Biller)
}
}
}
}
private companion object {
private const val TIMEOUT_MILLIS = 2000L
private const val RETRY_MILLIS = 3000L
}
}
在这种情况下,我们需要使用SharedFlow,它支持发出连续的重复值。
总结
本文主要是关于SharedFlow与StateFlow的一些介绍及使用他们代替LiveData的一些尝试
这是在下翻译的第一篇文章,还有很多不足之处,请各位读者结合原文观看,如果有什么总量,欢迎提出指正。