ankuranurag2
ankuranurag2

Reputation: 2441

Coroutine flows collection with no replay if already consumed

I have a StateFlow in my ViewModel like this:

    private val _walletState = MutableStateFlow<ResultModel<WalletResponse>>(ResultModel.loading())
    val walletState: StateFlow<ResultModel<WalletResponse>> = _walletState.asStateFlow()

A new value is emitted in this flow on API response.

I am observing this flow in my Fragment class in a lifecycle-aware manner using:

lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.CREATED){
            viewmodel.walletState.collect{
            if(it.status==LOADING)
                playSomeAnimation()
            else
                navigateToNextFragment()
        }
    }
}

The issue:

If my app is in the background and API call arrives, my collector is invoked, and I try to navigate to next fragment. But obviously, it crashes with IllegalStateException because I was trying to do fragment transaction after my onSavedInstanceState had been called.

I can solve this exception using repeatOnLifecycle(Lifecycle.State.RESUMED) instead of repeatOnLifecycle(Lifecycle.State.STARTED). But then the challenge is that every time my fragment is resumed, my collector is invoked. So, if I am starting an animation when the state is ResultModel.Loading, whenever I go to the background and come back to the app, it is resumed and my collector is invoked again making my animation run again.

Question:

How can I make my flow collection lifecycle aware with replay=1 but it should not be replayed on collection, once consumed by collector.

PS: I don't want any workaround solutions or hacks like keeping a boolean flag or resetting the state to a default value.

Upvotes: 1

Views: 159

Answers (3)

ltp
ltp

Reputation: 563

I think Flow.take() is what you need:

//It's also recommended to use viewLifecycleOwner when using
//repeatOnLifecycle inside Fragment
viewLifecycleOwner.lifecycleScope.launch {  
     viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
         viewmodel.walletState.take(1).collect { state ->
             //rest of the codes
        }
}

Upvotes: 1

Jan Itor
Jan Itor

Reputation: 4276

Technically, you could navigate even after onSaveInstanceState by setting allowStateLoss to true in FragmentManager.commit. But it is a bug prone approach I wouldn't recommend.

That leaves us with using repeatOnLifecycle(Lifecycle.State.RESUMED). As I understand the main problem in this case is the repeating animation. We could avoid dealing with flows and states and simply add a boolean flag to the ViewModel to control the animation. Call playSomeAnimation() only if flag is false and then set it to true. If necessary, reset the flag in navigateToNextFragment(). This is a simple approach, but it may be sufficient.

Upvotes: 1

Squti
Squti

Reputation: 4497

You need to define a hasStateConsumed flag in your ViewModel and notify your ViewModel that you have consumed the state every time you collect your state and check that flag before you use the state emissions like this:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.RESUMED) {
        viewmodel.walletState.collect { state ->
            if (!viewModel.hasStateConsumed) {
                if (state.status == LOADING) {
                    playSomeAnimation()
                } else {
                    navigateToNextFragment()
                }
                viewmodel.stateConsumed(true)
            }
        }
    }
}

And in your ViewModel do it like so:

class MyViewModel() : ViewModel() {
    private val _walletState = MutableStateFlow<ResultModel<WalletResponse>>(ResultModel.loading())
    val walletState: StateFlow<ResultModel<WalletResponse>> = _walletState.asStateFlow()
    var hasStateConsumed = false
        private set

    fun stateConsumed(isConsumed: Boolean) {
        hasStateConsumed = isConsumed
    }
    ...
}

Every time you update your _walletState in your ViewModel call stateConsumed(false) before it like this:

stateConsumed(false)
_walletState.update { 
    ...
}

Upvotes: 1

Related Questions