Kotlin Flow 每次数据发射(含下拉刷新)时发送Loading状态
问题背景
仓库层与ViewModel层现有实现如下:
- 仓库层数据流方法:
override fun getTopRatedMoviesStream(): Flow<List<Movie>> - 通用Result结果封装类:
sealed interface Result<out T> { data class Success<T>(val data: T) : Result<T> data class Error(val exception: Throwable? = null) : Result<Nothing> object Loading : Result<Nothing> } fun <T> Flow<T>.asResult(): Flow<Result<T>> { return this .map<T, Result<T>> { Result.Success(it) } .onStart { emit(Result.Loading) } .catch { emit(Result.Error(it)) } }
- ViewModel层UiState逻辑:
data class HomeUiState( val topRatedMovies: TopRatedMoviesUiState, val isRefreshing: Boolean ) @Immutable sealed interface TopRatedMoviesUiState { data class Success(val movies: List<Movie>) : TopRatedMoviesUiState object Error : TopRatedMoviesUiState object Loading : TopRatedMoviesUiState } class HomeViewModel @Inject constructor( private val movieRepository: MovieRepository ) : ViewModel() { private val topRatedMovies: Flow<Result<List<Movie>>> = movieRepository.getTopRatedMoviesStream().asResult() private val isRefreshing = MutableStateFlow(false) val uiState: StateFlow<HomeUiState> = combine( topRatedMovies, isRefreshing ) { topRatedResult, refreshing -> val topRated: TopRatedMoviesUiState = when (topRatedResult) { is Result.Success -> TopRatedMoviesUiState.Success(topRatedResult.data) is Result.Loading -> TopRatedMoviesUiState.Loading is Result.Error -> TopRatedMoviesUiState.Error } HomeUiState(topRated, refreshing) } .stateIn( scope = viewModelScope, started = WhileUiSubscribed, initialValue = HomeUiState( TopRatedMoviesUiState.Loading, isRefreshing = false ) ) fun onRefresh() { viewModelScope.launch(exceptionHandler) { movieRepository.refreshTopRated() isRefreshing.emit(true) isRefreshing.emit(false) } } }
现存问题:TopRatedMoviesUiState.Loading仅在Flow首次订阅时触发,用户下拉刷新、数据流推送新数据时不会触发该Loading状态,根因是onStart操作符仅在首次订阅时执行。
核心疑问:
- 执行刷新操作时是否需要重新订阅Flow?
- 刷新后仓库层无新数据返回时,如何避免重复状态发射?
解答
刷新时是否需要重新订阅Flow
不需要重新订阅Flow。getTopRatedMoviesStream()本身是响应式持久化数据流,只要刷新操作触发了仓库层底层数据源更新,数据流会自动推送最新值,重新订阅只会破坏响应式链路、增加不必要的资源开销。
原有逻辑的核心问题是混淆了两类加载态:
- 首次加载的全屏Loading:页面无历史数据时的占位加载态,对应
TopRatedMoviesUiState.Loading - 下拉刷新的加载态:页面已有内容时的刷新指示,对应你已经定义的
isRefreshing字段
二者不需要复用同一个状态,刷新时直接用isRefreshing控制下拉刷新组件的显示即可,不需要强制数据流重新发射Loading值,否则会导致已有列表内容被全屏Loading覆盖,体验劣化。
避免重复状态发射的实现方案
- 修正状态映射逻辑,保留已有数据
当列表已经加载成功过一次后,后续刷新过程中、刷新出错时都不要重置列表状态,仅通过isRefreshing控制刷新头,通过轻量提示(Toast/Snackbar)通知刷新失败即可,避免无意义的状态切换。 - 追加去重操作符,过滤重复发射
- 给仓库层数据流加
distinctUntilChanged,前后两次返回的电影列表内容完全一致时,不向下游发射数据:
private val topRatedMovies: Flow<Result<List<Movie>>> = movieRepository.getTopRatedMoviesStream() .distinctUntilChanged() .asResult()- 给UiState流加
distinctUntilChanged,前后两次组合出的HomeUiState完全相同时,不通知UI更新:
val uiState: StateFlow<HomeUiState> = combine( topRatedMovies, isRefreshing ) { topRatedResult, refreshing -> val topRated: TopRatedMoviesUiState = when (topRatedResult) { is Result.Success -> TopRatedMoviesUiState.Success(topRatedResult.data) is Result.Loading -> TopRatedMoviesUiState.Loading is Result.Error -> TopRatedMoviesUiState.Error } HomeUiState(topRated, refreshing) } .distinctUntilChanged() .stateIn(/* 原有配置保持不变 */) - 给仓库层数据流加
- 修正刷新逻辑的执行顺序,避免状态闪跳
原有逻辑先执行刷新请求再修改刷新状态,会导致刷新头显示延迟,同时异常场景下刷新状态无法正确重置,调整为如下实现:
用fun onRefresh() { viewModelScope.launch(exceptionHandler) { isRefreshing.emit(true) try { movieRepository.refreshTopRated() } finally { isRefreshing.emit(false) } } }finally块保证无论刷新成功、失败、被取消,isRefreshing状态都能正确重置,不会出现刷新头一直转动的问题。
内容的提问来源于stack exchange,提问作者liminal
相关产品推荐
相关产品推荐

