You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Activity后台/跳转时取消Kotlin Flows的API请求?

正确取消StateFlow上游流的实现方式

我曾尝试用stateIn结合StateFlow来取消上游流,但发现ViewModel的viewModelScope只会在Activity的onDestroy时销毁,导致API请求在Activity进入后台后仍会继续执行。目前我用的解决方案是在Activity的onStop中调用viewModelStore.clear(),但不确定这是否是正确做法。

以下是我当前的代码实现:

ViewModel中的状态流

val uiState = productsUseCase.getProductsData()
    .map {
        when (it) {
            is Result.Error -> {
                DashboardState.Error(it.exception.message.toString())
            }
            is Result.Success -> {
                DashboardState.Success(it.data.products)
            }
        }
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(0), DashboardState.Loading("Loading Products"))

数据获取逻辑

fun getProductsData(): Flow<Result<ProductsResponse>> {
    println("Fetching prods")
    return flow<Result<ProductsResponse>> {
        delay(3000)
        val productResponse = productService.fetchProducts()
        println("done Fetching prods")

        emit(Result.Success(productResponse))
    }.retry(2) {
        if (it is java.lang.Exception) {
            delay(DELAY_3S)
        }
        return@retry true
    }.catch {
        emit(Result.Error(it))
    }
}

Activity中的观察逻辑

lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.CREATED) {
            productsViewModel.uiState.collect {
                when (it) {
                    is DashboardState.Error -> {
                        recyclerView.gone()
                        progressBar.gone()
                        tvErrorMessage.visible()
                        tvErrorMessage.text = it.error
                        Log.d("MainActivity", "Error: ${it.error}")
                    }
                    is DashboardState.Loading -> {
                        Log.d("MainActivity", "Loading: ${it.loadingMessage}")
                    }
                    is DashboardState.Success -> {
                        Log.d("MainActivity", "Success: ${it.products.size}")
                        productsAdapter.submitList(it.products)
                        recyclerView.visible()
                        progressBar.gone()
                        tvErrorMessage.gone()
                    }
                }
            }
        }
    }

核心问题分析

你当前的问题根源有两个:

  1. repeatOnLifecycle使用了CREATED状态:这个状态在Activity进入后台(onStop)时仍然保持活跃,订阅不会取消,导致stateIn的WhileSubscribed逻辑无法触发。
  2. 滥用viewModelStore.clear():这个操作会直接销毁ViewModel,导致页面重新进入时丢失之前的状态,完全违背了ViewModel的设计初衷。

正确实现步骤

1. 调整repeatOnLifecycle的生命周期状态

将Activity中的观察逻辑改为使用Lifecycle.State.STARTED,这样当Activity进入后台(onStop)时,订阅会自动取消,触发stateIn的停止逻辑。

2. 合理配置SharingStarted.WhileSubscribed的超时参数

WhileSubscribed(stopTimeoutMillis)的作用是:当最后一个订阅者取消后,等待指定时间如果没有新订阅,就停止上游流。设置一个合理的超时(比如5000ms),既可以避免用户快速切后台再回来时重复请求,又能在后台及时停止不必要的API调用。

修改后的代码示例

ViewModel中的状态流

val uiState = productsUseCase.getProductsData()
    .map {
        when (it) {
            is Result.Error -> {
                DashboardState.Error(it.exception.message.toString())
            }
            is Result.Success -> {
                DashboardState.Success(it.data.products)
            }
        }
    }
    .stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000), // 5秒超时后停止上游
        DashboardState.Loading("Loading Products")
    )

Activity中的观察逻辑

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) { // 改用STARTED状态
        productsViewModel.uiState.collect {
            when (it) {
                is DashboardState.Error -> {
                    recyclerView.gone()
                    progressBar.gone()
                    tvErrorMessage.visible()
                    tvErrorMessage.text = it.error
                    Log.d("MainActivity", "Error: ${it.error}")
                }
                is DashboardState.Loading -> {
                    Log.d("MainActivity", "Loading: ${it.loadingMessage}")
                }
                is DashboardState.Success -> {
                    Log.d("MainActivity", "Success: ${it.products.size}")
                    productsAdapter.submitList(it.products)
                    recyclerView.visible()
                    progressBar.gone()
                    tvErrorMessage.gone()
                }
            }
        }
    }
}

额外说明

  • 如果希望订阅取消后立即停止上游,可以将WhileSubscribed的超时设为0,但可能会导致用户快速切换前后台时重复发起请求,需要根据业务场景权衡。
  • 绝对不要使用viewModelStore.clear()来解决这个问题,它会销毁ViewModel实例,丢失所有保存的状态,是非常不合理的做法。

内容的提问来源于stack exchange,提问作者Veeresh Charantimath

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 20:27:31