简体   繁体   English

Kotlin Coroutine Flow: 限制收集器数量

[英]Kotlin Coroutine Flow: Limit the number of collector

Is there a way to limit the number of collector in a function that returns a Flow using flow builder?有没有办法限制 function 中使用流构建器返回流的收集器数量?

I have this public method in a ViewModel我在 ViewModel 中有这个公共方法

fun fetchAssets(limit: String) {

        viewModelScope.launch {

            withContext(Dispatchers.IO){
                getAssetsUseCase(AppConfigs.ASSET_PARAMS, limit).onEach {

                    when (it) {

                        is RequestStatus.Loading -> {
                            _assetState.tryEmit(AssetState.FetchLoading)
                        }

                        is RequestStatus.Success -> {
                            _assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
                        }

                        is RequestStatus.Failed -> {
                            _assetState.tryEmit(AssetState.FetchFailed(it.message))
                        }

                    }

                }.collect()
            }

        }

    }

This method is called on ViewModel's init block, but can also be called manually on UI.此方法在 ViewModel 的init块上调用,但也可以在 UI 上手动调用。

This flow emits value every 10 seconds.此流每 10 秒发出一次值。

Repository资料库

override fun fetchAssets(
        query: String,
        limit: String
    ) = flow {
        while (true) {
            try {
                interceptor.baseUrl = AppConfigs.ASSET_BASE_URL
                emit(RequestStatus.Loading())
                val domainModel = mapper.mapToDomainModel(service.getAssetItems(query, limit))
                emit(RequestStatus.Success(domainModel))
            } catch (e: HttpException) {
                emit(RequestStatus.Failed(e))
            } catch (e: IOException) {
                emit(RequestStatus.Failed(e))
            }
            delay(10_000)
        }
    }

Unfortunately every time fetch() was invoke from UI, I noticed that it creates another collectors thus can ended up having tons of collector which is really bad and incorrect.不幸的是,每次从 UI 调用fetch()时,我都注意到它会创建另一个收集器,因此最终可能会拥有大量收集器,这非常糟糕且不正确。

The idea is having a flow that emits value every 10 seconds but can also be invoke manually via UI for immediate data update without having multiple collectors.这个想法是让一个流每 10 秒发出一次值,但也可以通过 UI 手动调用以立即更新数据,而无需多个收集器。

You seem to misunderstand what does it mean to collect the flow or you misuse the collect operation.您似乎误解了 collect flow 是什么意思,或者您滥用了 collect 操作。 By collecting the flow we mean we observe it for changes.通过收集流量,我们的意思是我们观察它的变化。 But you try to use collect() to introduce changes to the flow, which can't really work.但是你尝试使用collect()来引入流程的变化,这并不能真正起作用。 It just starts another flow in the background.它只是在后台启动另一个流程。

You should collect the flow only once, so keep it inside init or wherever it is appropriate for your case.您应该只收集一次流,因此将其保存在init中或适合您的情况的任何地方。 Then you need to update the logic of the flow to make it possible to trigger reloading on demand.然后你需要更新流程的逻辑,以使其能够触发按需重新加载。 There are many ways to do it and the solution will differ depending whether you need to reset the timer on manual update or not.有很多方法可以做到这一点,解决方案会有所不同,具体取决于您是否需要在手动更新时重置计时器。 For example, we can use the channel to notify the flow about the need to reload:例如,我们可以使用通道通知流需要重新加载:

val reloadChannel = Channel<Unit>(Channel.CONFLATED)

fun fetchAssets(
    query: String,
    limit: String
) = flow {
    while (true) {
        try {
            ...
        }
        
        withTimeoutOrNull(10.seconds) { reloadChannel.receive() } // replace `delay()` with this
    }
}

fun reload() {
    reloadChannel.trySend(Unit)
}

Whenever you need to trigger the manual reload, do not start another flow or invoke another collect() operation, but instead just invoke reload() .每当您需要触发手动重新加载时,不要启动另一个流程或调用另一个collect()操作,而只是调用reload() Then the flow that is already being collected, will start reloading and will emit state changes.然后已经被收集的流将开始重新加载并发出 state 变化。

This solution resets the timer on manual reload, which I believe is better for the user experience.此解决方案会在手动重新加载时重置计时器,我认为这对用户体验更好。

I ended up moving the timer on ViewModel as I can request on demand fetch while also not having multiple collectors that runs at the same time.我最终在 ViewModel 上移动了计时器,因为我可以请求按需获取,同时也没有同时运行的多个收集器。

private var job: Job? = null

    private val _assetState = defaultMutableSharedFlow<AssetState>()

    fun getAssetState() = _assetState.asSharedFlow()

    init {
        job = viewModelScope.launch {
            while(true) {
                if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
                    fetchAssets()
                delay(10_000)
            }
        }
    }

    fun fetchAssets() {

        viewModelScope.launch {

            withContext(Dispatchers.IO) {
                getAssetsUseCase(
                    AppConfigs.ASSET_BASE_URL,
                    AppConfigs.ASSET_PARAMS,
                    AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
                ).onEach {

                    when(it){

                        is RequestStatus.Loading -> {
                            _assetState.tryEmit(AssetState.FetchLoading)
                        }

                        is RequestStatus.Success -> {
                            _assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
                        }

                        is RequestStatus.Failed -> {
                            _assetState.tryEmit(AssetState.FetchFailed(it.message))
                        }

                    }

                }.collect()
            }

        }

    }

    override fun onCleared() {
        job?.cancel()
        super.onCleared()
    }

Please correct me if this one is a code smell.如果这是代码味道,请纠正我。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM