简体   繁体   中英

Kotlin Flow: emit loading state for every emission

My repo has the following function:

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

I have the following Result wrapper:

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)) }
}

And finally, my ViewModel has the following UiState logic:

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)
    }
  }

The issue is the TopRatedMoviesUiState.Loading state is only emitted once on initial load but not when user pulls to refresh and new data is emitted in movieRepository.getTopRatedMoviesStream() . I understand that it is because .onStart only emits first time the Flow is subscribed to.

Do I somehow resubscribe to Flow when refresh is performed? Refresh does not always return new data from repo so how in this case, how do I avoid duplicate emission?

You emit TopRatedMoviesUiState.Loading in onStart . So what you describe is totally expected. Loading is emitted when the stream starts. Ie when you start collecting. In your case by stateIn .

Looking at it from another perspective, how should your result wrapper know that the repository is currently loading? And that's also the answer. Only two places in your code know that you are reloading.

  • Either subscribe to a fresh flow whenever you call refreshTopRated and complete the flow after emitting the final result.
  • Or emit a loading state right from the repository before you start loading.

I'd prefer the later one.

Neither solution will save you from emitting the same result again and again. For that, you'd need a new state like 'Unchanged' that your repository emits when it finds no new data. But please evaluate if this optimization is required. What is the cost of an extra emission?

That being said, here are some more questions, challenging your code. Hopefully guiding you to a solid implementation:

  • Why do you need the isRefreshing StateFlow? Is there a difference in the UI wether you initially load or wether you refresh?
  • Why do you need topRated to be a StateFlow? Wouldn't a regular Flow do?
  • Emitting two items to a StateFlow in succession (ie isRefreshing.emit(true); isRefreshing.emit(false) ) might loose the first emission [1]. As for state, only the most recent value is relevant. States are not events!

[1]: StateFlow has a replay buffer of 1 and a buffer overflow policy of DROP_OLDEST. See State flow is a shared flow in kotlinx-coroutines-core/kotlinx.coroutines.flow/StateFlow

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM