如何在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() } } } } }
核心问题分析
你当前的问题根源有两个:
repeatOnLifecycle使用了CREATED状态:这个状态在Activity进入后台(onStop)时仍然保持活跃,订阅不会取消,导致stateIn的WhileSubscribed逻辑无法触发。- 滥用
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
相关产品推荐
相关产品推荐

