Home > Net >  Kotlin SharedFlow ViewModel emits before subscribed
Kotlin SharedFlow ViewModel emits before subscribed

Time:04-27

I am trying to use SharedFlow as data provider for a Fragment in MVVM architecture.

In the Fragment class:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.data.collect { value ->
                    handleData(data)
                }
            }
        }
        viewModel.init()
    }

In the ViewModel class:

    private val _data: MutableSharedFlow<DataState> = MutableSharedFlow()
    val data: SharedFlow<DataState> = _data

    fun init() {
        ...
        //(listen for other data providers that generate data for SharedFlow)
        ...
        viewModelCoroutineScope.launch {
            val dataCollection = interactor.getDataCollection()
            dataCollection.forEach { data ->
                if (data != null) {
                    _data.emit(DataState(data = data))
                }
            }
        }
    }

The problem is that in 50% cases viewmodel.init() starts before subscriber under scope is connected to Flow - which results in some data lost. Why SharedFlow is used? That is because ViewModel have subscriptions to other data sources which could send a lot of data instances in the irregular way all needed to collect, so StateFlow/LiveData with their "store only last value" is not good for this.

When I've tried to pin viewmodel.init() to subscriber coroutine like this:

        val job = viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.data.collect { value ->
                    handleData(data)
                }
            }
        }
        viewLifecycleOwner.lifecycleScope.launch {
            job.join()
            viewModel.init()
        }

the ViewModel emits data, but Fragment is never collects it.

What is right way to guarantee that subscribers is on before call of the ViewModel to start data sending through SharedFlow?

CodePudding user response:

You should give your SharedFlow a replay value of 1 so late subscribers will still get the most recent value. You need this anyway. If the screen rotates, the recreated Fragment will need the latest value to show in the UI.

private val _data: MutableSharedFlow<DataState> = MutableSharedFlow(replay = 1)

But actually, it would be better to use shareIn instead of MutableSharedFlow, because then you can pause collection when there are no active subscribers, so you can avoid unnecessary monitoring of resources when the associated Fragment is off-screen. Like this:

val data: SharedFlow<DataState> = interactor.getDataCollection()
    .mapNotNull { it?.let(::DataState) }
    .shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)

If getDataCollection() is a suspend function, you could do it like this:

val data: SharedFlow<DataState> = flow {
        interactor.getDataCollection().emitAll()
    }
    .mapNotNull { it?.let(::DataState) }
    .shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)

If it's not a suspend function, why do you have a getter function at all? Kotlin uses properties instead.

  • Related