Kotlin StateFlow切后台再前台重复请求API的问题求助
解决Kotlin Flow后台恢复后重复API请求的问题
问题场景
使用Kotlin Flow实现API调用,ViewModel通过StateFlow向Activity暴露数据。应用启动时API请求正常,Activity能成功收集数据;但将应用置于后台5秒后再打开,API会再次发起请求,需要避免这种重复请求。
现有代码
ViewModel代码
class FlowViewModel( dataSource: DataSource ) : BaseViewModel<UiState>() { val priceFlow: StateFlow<UiState> = dataSource .apiCall .onStart { Timber.tag("Flow").d("Flow has started.") } .map { stockList -> UiState.Success(stockList) as UiState } .onCompletion { Timber.tag("Flow").d("Flow has completed.") }.stateIn( scope = viewModelScope, initialValue = UiState.Loading, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000) ) }
Activity收集Flow代码
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.priceFlow.collect { uiState -> render(uiState) } } }
模拟DataSource代码
class DataSource(mockApi: MockApi) : BaseDataSource { override val apiCall: Flow<List<Price>> = flow { val currentStockList = mockApi.getCurrentPrices() emit(currentStockList) } }
问题原因
- StateFlow共享策略:当前使用
SharingStarted.WhileSubscribed(5000),表示最后一个订阅者(Activity)取消订阅后,等待5秒就会停止Flow共享,上游的API请求Flow会被取消。 - 冷Flow特性:
apiCall是一个冷Flow,每次被订阅时都会重新执行代码块,发起新的API请求。当应用回到前台,Activity重新订阅StateFlow,会触发上游冷Flow再次执行,导致重复请求。
解决方案
可以通过以下两种核心思路解决,根据业务需求选择:
方案一:修改StateFlow的共享策略,保持共享状态
将stateIn的started参数改为SharingStarted.Lazily或设置无限长的超时时间,让Flow共享在ViewModel生命周期内持续保持,避免上游Flow被取消后重新执行:
// 方案1a:使用Lazily,第一次订阅启动后持续共享直到ViewModel销毁 .stateIn( scope = viewModelScope, initialValue = UiState.Loading, started = SharingStarted.Lazily ) // 方案1b:设置无限超时,避免订阅取消后停止共享 .stateIn( scope = viewModelScope, initialValue = UiState.Loading, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = Long.MAX_VALUE) )
方案二:缓存API请求结果,避免重复调用
在DataSource或ViewModel中缓存API结果,即使Flow重新订阅,也直接返回缓存数据而不是发起新请求:
修改DataSource缓存结果
将apiCall转为热Flow,只请求一次并缓存结果:
class DataSource(mockApi: MockApi) : BaseDataSource { override val apiCall: Flow<List<Price>> = flow { val currentStockList = mockApi.getCurrentPrices() emit(currentStockList) }.shareIn( scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), started = SharingStarted.Lazily, // 第一次订阅时发起请求 replay = 1 // 缓存最新的1条结果 ) }
在ViewModel中缓存结果
如果需要更灵活的缓存控制,可在ViewModel中存储已请求的数据:
class FlowViewModel( dataSource: DataSource ) : BaseViewModel<UiState>() { private var cachedStockList: List<Price>? = null val priceFlow: StateFlow<UiState> = flow { // 优先使用缓存数据 cachedStockList?.let { emit(UiState.Success(it)) return@flow } // 无缓存时发起请求并缓存 val stockList = dataSource.apiCall.first() cachedStockList = stockList emit(UiState.Success(stockList)) }.stateIn( scope = viewModelScope, initialValue = UiState.Loading, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000) ) }
内容的提问来源于stack exchange,提问作者Darshan Mistry
相关产品推荐
相关产品推荐

