头图

Flow.shareIn and Flow.stateIn operators can convert cold flow to hot flow: they can broadcast information from upstream cold data streams to multiple collectors. These two operators are usually used to improve performance: add a buffer when there is no collector; or simply use it as a caching mechanism.

Note that : cold flow is created on demand, and will send data when they are observed; hot flow are always active regardless of whether they are being observed.

This article will use examples to help you familiarize yourself with the shareIn and stateIn operators. You will learn how to configure them for specific use cases and avoid common pitfalls that you may encounter.

underlying data stream producer

I continue to use previous article example used - using the underlying data stream producers sent a location update. It is a use callbackFlow implemented cold . Each new collector will trigger the producer code block of the data stream, and will also add a new callback to the FusedLocationProviderClient.

class LocationDataSource(
    private val locationClient: FusedLocationProviderClient
) {
    val locationsSource: Flow<Location> = callbackFlow<Location> {
        val callback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult?) {
                result ?: return
                try { offer(result.lastLocation) } catch(e: Exception) {}
            }
        }
        requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
            .addOnFailureListener { e ->
                close(e) // in case of exception, close the Flow
            }
        // 在 Flow 结束收集时进行清理
        awaitClose {
            removeLocationUpdates(callback)
        }
    }
}

Let's see how to use shareIn and stateIn to optimize the data flow of locationsSource in different use cases.

shareIn or stateIn?

The first topic we will discuss is the difference between shareIn and stateIn shareIn operator returns SharedFlow and stateIn returns StateFlow .

Note : To learn more about StateFlow and SharedFlow more information, you can check our documentation .

StateFlow is a special configuration of SharedFlow, designed to optimize the sharing state: the last items sent will be resent to the new collector, and these items will be merged Any.equals You can find more information StateFlow document

The main difference between the two is that the StateFlow interface allows you to synchronously access the last value sent value This is not SharedFlow is used.

Improve performance

By sharing the same data flow instance that all collectors want to observe (rather than creating new instances of the same data flow on demand), these APIs can improve performance for us.

In the following example, LocationRepository consumes the LocationDataSource data stream exposed by locationsSource , and uses the shareIn operator, so that every collector interested in user location information collects data from the same data stream instance. locationsSource data stream instance is created here and shared by all collectors:

class LocationRepository(
    private val locationDataSource: LocationDataSource,
    private val externalScope: CoroutineScope
) {
    val locations: Flow<Location> = 
        locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}

WhileSubscribed The sharing strategy is used to cancel the upstream data flow when there is no collector. In this way, we can avoid wasting resources when there is no program interested in location updates.

Android application reminder! In most cases, you can use WhileSubscribed(5000) keep the upstream data stream active for 5 seconds after the last collector disappears. This can avoid restarting the upstream data stream in certain specific situations (such as configuration changes). This technique is especially useful when the upstream data flow is expensive to create, or when these operators are used in the ViewModel.

buffer event

In the example below, our requirements have changed. Now we keep listening for location updates, and to display the last 10 locations on the screen when the app returns to the foreground from the background:

class LocationRepository(
    private val locationDataSource: LocationDataSource,
    private val externalScope: CoroutineScope
) {
    val locations: Flow<Location> = 
        locationDataSource.locationsSource
            .shareIn(externalScope, SharingStarted.Eagerly, replay = 10)
}

We set replay to 10 to keep the last 10 items sent in the memory and resend these items every time a collector observes the data stream. In order to keep the internal data stream always active and send location updates, we use the sharing strategy SharingStarted.Eagerly , so that even if there is no collector, we can always listen for updates.

Cache data

Our requirements have changed again. This time we no longer need continue monitor location updates when the app is in the background. However, we need to cache the last item sent so that the user can see some data on the screen when getting the current location (even if the data is old). In this case, we can use the stateIn operator.

class LocationRepository(
    private val locationDataSource: LocationDataSource,
    private val externalScope: CoroutineScope
) {
    val locations: Flow<Location> = 
        locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}

Flow.stateIn can buffer the last sent item and replay it to new collectors.

Attention! Do not create a new instance

Do when calling a function call returns, use shareIn or stateIn create a new data stream. This will create a new SharedFlow or StateFlow each time the function is called, and they will remain in memory until the scope is cancelled or garbage collected when there are no references.

class UserRepository(
    private val userLocalDataSource: UserLocalDataSource,
    private val externalScope: CoroutineScope
) {
    // 不要像这样在函数中使用 shareIn 或 stateIn 
    // 这将在每次调用时创建新的 SharedFlow 或 StateFlow,而它们将不会被复用。
    fun getUser(): Flow<User> =
        userLocalDataSource.getUser()
            .shareIn(externalScope, WhileSubscribed())    

    // 可以在属性中使用 shareIn 或 stateIn 
    val user: Flow<User> = 
        userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}

The data stream that needs to be entered

userId (such as 06125c04de8923) cannot simply be shared shareIn or stateIn Take the open source project-Google I/O's Android application iosched as an example. You can see source code of . The data flow of user events obtained Firestore callbackFlow . Because it receives userId as a parameter, it cannot be multiplexed by shareIn or stateIn

class UserRepository(
    private val userEventsDataSource: FirestoreUserEventDataSource
) {
    // 新的收集者会在 Firestore 中注册为新的回调。
    // 由于这一函数依赖一个 `userId`,所以在这个函数中
    // 数据流无法通过调用 shareIn 或 stateIn 进行复用.
    // 这样会导致每次调用函数时,都会创建新的  SharedFlow 或 StateFlow
    fun getUserEvents(userId: String): Flow<UserEventsResult> =
        userLocalDataSource.getObservableUserEvents(userId)
}

How to optimize this use case depends on the needs of your application:

  • Do you allow to receive events from multiple users at the same time? If the answer is yes, you may need to create a map SharedFlow or StateFlow instance, and remove the reference and exit the upstream data stream when subscriptionCount
  • If you only allow one user, and the collector needs to be updated to observe new users, you can send an SharedFlow or StateFlow shared by all collectors, and use the public data stream as a variable in the class.

shareIn and stateIn operators can be used with cold flow to improve performance. You can use them to add buffers when there is no collector, or use them directly as a caching mechanism. Use them carefully, do not create a new data flow instance every time the function is called-this will lead to waste of resources and unexpected problems!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。