【译】取代LiveData:StateFlow还是SharedFlow?

6,502 阅读14分钟

前言

原标题: 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的一些尝试
这是在下翻译的第一篇文章,还有很多不足之处,请各位读者结合原文观看,如果有什么总量,欢迎提出指正。