【问题标题】:Kotlin Flow: emit loading state for every emissionKotlin Flow:为每次发射发射加载状态
【发布时间】:2022-11-11 03:33:44
【问题描述】:

我的回购具有以下功能:

override fun getTopRatedMoviesStream(): Flow<List<Movie>>

我有以下结果包装器:

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 状态仅在初始加载时发出一次,但在用户拉动刷新并在movieRepository.getTopRatedMoviesStream() 中发出新数据时不会发出。我知道这是因为.onStart 仅在第一次订阅 Flow 时发出。

执行刷新时,我是否以某种方式重新订阅 Flow?刷新并不总是从 repo 返回新数据,那么在这种情况下,如何避免重复发射?

【问题讨论】:

    标签: android kotlin-coroutines kotlin-flow


    【解决方案1】:

    你在onStart 中发出TopRatedMoviesUiState.Loading。所以你所描述的完全是意料之中的。 Loading 在流启动时发出。 IE。当你开始收集。在你的情况下,stateIn

    从另一个角度来看,您的结果包装器应该如何知道存储库当前正在加载? 这也是答案。您的代码中只有两个地方知道您正在重新加载。

    • 要么在调用 refreshTopRated 时订阅新流程,要么在发出最终结果后完成流程。
    • 或者在开始加载之前直接从存储库发出加载状态。

    我更喜欢后者。

    这两种解决方案都不会让您一次又一次地发出相同的结果。为此,您需要一个新状态,例如“未更改”,当您的存储库找不到新数据时会发出该状态。但请评估是否需要此优化。额外排放的成本是多少?

    话虽如此,这里还有一些问题,挑战您的代码。希望能指导您进行可靠的实施:

    • 为什么需要isRefreshing StateFlow?无论您最初加载还是刷新,UI 是否存在差异?
    • 为什么需要topRated 成为StateFlow?普通的Flow不行吗?
    • 连续向 StateFlow 发射两个项目(即isRefreshing.emit(true); isRefreshing.emit(false))可能会丢失第一个发射 [1]。至于状态,只有最近的值是相关的。状态不是事件!

    [1]:StateFlow 的重放缓冲区为 1,缓冲区溢出策略为 DROP_OLDEST。看状态流是共享流kotlinx-coroutines-core/kotlinx.coroutines.flow/StateFlow

    【讨论】:

      【解决方案2】:

      您可以将您的数据流 (moviesStream) 和加载流结合到Result

      在开始收集流之前,您可以使用moviesStream().onEach {loading.value = false},而不是只调用一次的flow.onStart,它

      返回在每个值之前调用给定操作的流 上游流向下游排放

      ,因此在收集存储库数据之前,加载状态将设置为 false。

      private val topRatedMovies: Flow<List<Movie>> = movieRepository.getTopRatedMoviesStream().onEach {loading.value = false}
      
      val uiState = combineToResult(topRatedMovies, isRefreshing)
              .map {result ->
                  when(result){
                      is Result.Loading -> ...
                      is Result.Success -> ...
                      is Result.Error -> ...
                  }
              }
              
      fun onRefresh() {
          viewModelScope.launch(exceptionHandler) {
              isRefreshing.emit(true)
              movieRepository.refreshTopRated()
          }
      }
      
      fun <T> combineToResult(contentFlow: Flow<T>, loadingFlow: Flow<Boolean>): Flow<Result<T>> {
          return combine(contentFlow, loadingFlow) { content, loading ->
              if (loading) {
                  Result.Loading
              } else {
                  Result.Success(content)
              }
          }.catch { emit(Result.Error(it)) }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-01-12
        • 1970-01-01
        相关资源
        最近更新 更多