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

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操作符仅在首次订阅时执行。
核心疑问:

  1. 执行刷新操作时是否需要重新订阅Flow?
  2. 刷新后仓库层无新数据返回时,如何避免重复状态发射?
解答

刷新时是否需要重新订阅Flow

不需要重新订阅Flow。
getTopRatedMoviesStream()本身是响应式持久化数据流,只要刷新操作触发了仓库层底层数据源更新,数据流会自动推送最新值,重新订阅只会破坏响应式链路、增加不必要的资源开销。
原有逻辑的核心问题是混淆了两类加载态:

  • 首次加载的全屏Loading:页面无历史数据时的占位加载态,对应TopRatedMoviesUiState.Loading
  • 下拉刷新的加载态:页面已有内容时的刷新指示,对应你已经定义的isRefreshing字段
    二者不需要复用同一个状态,刷新时直接用isRefreshing控制下拉刷新组件的显示即可,不需要强制数据流重新发射Loading值,否则会导致已有列表内容被全屏Loading覆盖,体验劣化。

避免重复状态发射的实现方案

  • 修正状态映射逻辑,保留已有数据
    当列表已经加载成功过一次后,后续刷新过程中、刷新出错时都不要重置列表状态,仅通过isRefreshing控制刷新头,通过轻量提示(Toast/Snackbar)通知刷新失败即可,避免无意义的状态切换。
  • 追加去重操作符,过滤重复发射
    1. 给仓库层数据流加distinctUntilChanged,前后两次返回的电影列表内容完全一致时,不向下游发射数据:
    private val topRatedMovies: Flow<Result<List<Movie>>> =
      movieRepository.getTopRatedMoviesStream()
        .distinctUntilChanged()
        .asResult()
    
    1. 给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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 04:51:21