Reputation: 2441
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()
}
}
}
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.
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
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
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
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