So lately I've been working with StateFlow, SharedFlow, and Channels API's but I'm struggling with one common use case while trying to migrate my code from LiveData to StateFlow in the presentation layer.
The problem I'm facing is when I emit my data and collect it in viewModel so I can set the value to a mutableStateFlow, when it finally gets to the fragment it shows some informative messages using a Toast to let the user knows whether an error happened or everything went fine. Next, there's a button which navigates to another fragment, but if I go back to the previous screen which already has the result of the failed intent, again it displays the Toast. And that's exactly what I'm trying to figure out. If I collected already the result and showed the message to the user I don't want to keep doing it. If I navigate to another screen and return (it also happens when the app comes back from the background, it collects again the last value). This problem didn't happen with LiveData where I just did exact same thing, expose a flow from a repository and collected via LiveData in ViewModel.
Code:
class SignInViewModel @Inject constructor(
private val doSignIn: SigninUseCase
) : ViewModel(){
private val _userResult = MutableStateFlow<Result<String>?>(null)
val userResult: StateFlow<Result<String>?> = _userResult.stateIn(viewModelScope, SharingStarted.Lazily, null) //Lazily since it's just one shot operation
fun authenticate(email: String, password: String) {
viewModelScope.launch {
doSignIn(LoginParams(email, password)).collect { result ->
Timber.e("I just received this $result in viewmodel")
_userResult.value = result
}
}
}
}
Then in my Fragment:
override fun onViewCreated(...){
super.onViewCreated(...)
launchAndRepeatWithViewLifecycle {
viewModel.userResult.collect { result ->
when(result) {
is Result.Success -> {
Timber.e("user with code:${result.data} logged in")
shouldShowLoading(false)
findNavController().navigate(SignInFragmentDirections.toHome())
}
is Result.Loading -> {
shouldShowLoading(true)
}
is Result.Error -> {
Timber.e("error: ${result.exception}")
if(result.exception is Failure.ApiFailure.BadRequestError){
Timber.e(result.exception.message)
shortToast("credentials don't match")
} else {
shortToast(result.exception.toString())
}
shouldShowLoading(false)
}
}
}
}
launchAndRepeatWithViewLifecycle extension function:
inline fun Fragment.launchAndRepeatWithViewLifecycle(
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) {
block()
}
}
}
Any thoughts on why this happens and how to fancy solve it using StateFlow? I tried also with SharedFlow with replay = 0 and Channels with receiveAsFlow() but then other problems arise.