Nishant Jalan
Nishant Jalan

Reputation: 1038

SharedFlow is not collecting from emission

In my ViewModel, I am making API requests and I am using StateFlow and SharedFlow to communicate with the Fragment. While making the API request, I am easily able to update the state flow's value and it is successfully collected in the Fragment.

But before making the request, I am emitting some boolean values with SharedFlow and it is not getting collected in the Fragment. Can someone help me why is this happening?

class MainViewModel: ViewModel() {
  private val _stateFlow = MutableStateFlow(emptyList<Model>())
  val stateFlow = _stateFlow.asStateFlow()

  private val _loading = MutableSharedFlow<Boolean>()
  val loading = _loading.asSharedFlow()

  suspend fun request() {
    _loading.emit(true)
    withContext(Dispatchers.IO) {
      /* makes API request */
      /* updates _stateFlow.value */
      /* stateFlow value is successfully collected */
    }
    _loading.emit(false) // emitting boolean value
  }
}
class MyFragment : Fragment(R.layout.fragment_my) {
   // ...

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    lifecycleScope.launchWhenStarted {
      viewModel.request()
      /* stateFlow is collected and triggered - working properly */

      viewModel.loading.collectLatest { // <- NOT COLLECTING - WHY?
        Log.d(this::class.simpleName, "onViewCreated: $it") // <- NOT LOGGING
      }
    }
  }
}

Upvotes: 8

Views: 11558

Answers (3)

Weidian Huang
Weidian Huang

Reputation: 2935

SharedFlow by default doesn't replay any value when you subscribe to it.

   viewModel.request() // _loading.emit() has executed before collection.

   viewModel.loading.collectLatest { // No value can be replayed here
        Log.d(this::class.simpleName, "onViewCreated: $it") 
   }

So for the above code, if want to collect the last 2 values when you subscribe to it. You can do something like this.

private val _loading = MutableSharedFlow<Boolean>(replay = 2)

But you won't to see the loading effect, because 2 values will be collected at the same time when you subscribe to it. Probably this is what you want, _loading is emitted in IO thread, so the request() can be immediately returned before _loading is set, and you can start to collect the value at Fragment level:

suspend fun request() {
    withContext(Dispatchers.IO) {
      _loading.emit(true)
      /* makes API request */
      /* updates _stateFlow.value */
      /* stateFlow value is successfully collected */
      _loading.emit(false) // emitting boolean value
    }
  }

StateFlow is similar to LiveData, it can emit the last value when the new subscriber subscribes to it. Something like SharedFlow with replay = 1. That's why you can still collect the last value in the above code.

Upvotes: 4

CoolMind
CoolMind

Reputation: 28793

SharedFlow is a hot stream. Probably you should create it with

MutableSharedFlow(
    replay = 0,
    onBufferOverflow = BufferOverflow.DROP_OLDEST,
    extraBufferCapacity = 1
)

or

MutableSharedFlow(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

Upvotes: 11

Sergio
Sergio

Reputation: 30645

I guess you need to launch a different coroutine to collect loading values, something like the following:

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    lifecycleScope.launchWhenStarted {
      viewModel.request()
    }
    lifecycleScope.launchWhenStarted {    
      viewModel.loading.collectLatest { 
        Log.d(this::class.simpleName, "onViewCreated: $it")
      }
    }
  }

viewModel.request() function is a suspend function, it suspends the coroutine until it is finished. But I guess it is not finishing due to calling suspend function _loading.emit(), suspending until it is collected.


Or I think it is even better would be to launch a coroutine in ViewModel class, something like the following:

// In MainViewModel
fun request() = viewModelScope.launch {
    _loading.emit(true)
    withContext(Dispatchers.IO) {
      /* makes API request */
      /* updates _stateFlow.value */
      /* stateFlow value is successfully collected */
    }
    _loading.emit(false) // emitting boolean value
}

// In MyFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    lifecycleScope.launchWhenStarted {
      viewModel.loading.collectLatest { 
        Log.d(this::class.simpleName, "onViewCreated: $it") 
      }
    }

    viewModel.request()
}

Upvotes: 3

Related Questions