简体   繁体   中英

StateFlow last value is collected again in ui

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.

It looks like you would be looking for SingleLiveEvent with Kotlin flow.

` class MainViewModel : ViewModel() {

sealed class Event {
    data class ShowSnackBar(val text: String): Event()
    data class ShowToast(val text: String): Event()
}

private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()

init {
    viewModelScope.launch {
        eventChannel.send(Event.ShowSnackBar("Sample"))
        eventChannel.send(Event.ShowToast("Toast"))
    }
}

}

class MainFragment : Fragment() {

companion object {
    fun newInstance() = MainFragment()
}

private val viewModel by viewModels<MainViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    return inflater.inflate(R.layout.main_fragment, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // Note that I've chosen to observe in the tighter view lifecycle here.
    // This will potentially recreate an observer and cancel it as the
    // fragment goes from onViewCreated through to onDestroyView and possibly
    // back to onViewCreated. You may wish to use the "main" lifecycle owner
    // instead. If that is the case you'll need to observe in onCreate with the
    // correct lifecycle.
    viewModel.eventsFlow
        .onEach {
            when (it) {
                is MainViewModel.Event.ShowSnackBar -> {}
                is MainViewModel.Event.ShowToast -> {}
            }
        }
        .flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle, minActiveState = Lifecycle.State.STARTED)
        .onEach {
            // Do things
        }
        .launchIn(viewLifecycleOwner.lifecycleScope)
}

}

`

Credit : Michael Ferguson has authored a great article with the updated library enhancement. Would recommend you to go through it. I have copied the extract of it.

https://proandroiddev.com/android-singleliveevent-redux-with-kotlin-flow-b755c70bb055

You can create an extension function like this:

fun <T> MutableStateFlow<T?>.set(value: T, default: T? = null) {
    this.value = value
    this.value = default
}

It sets the desired default value after the new value has been emitted. In my case, I use null as the desired value.

Inside ViewModel instead of setting the value directly you can use the set() extension function.

fun signIn() = authRepository.signIn(phoneNumber.value).onEach {
    _signInState.set(it)
}.launchIn(viewModelScope)

You can set your state's value to the Idle(), which means an empty state

lifecycleScope.launch {
           viewModel.signInState.collect {
               when (it) {
                   is ViewState.Success -> {
                       stopLoading()
                       viewModel._signInState.value = ViewState.Idle()
                       // do your tasks here
                   }
                   is ViewState.Error<*> -> requireActivity().onBackPressedDispatcher.onBackPressed()
                   is ViewState.BusinessException<*> -> requireActivity().onBackPressedDispatcher.onBackPressed()
                   is ViewState.Idle<*> -> stopLoading()
                   is ViewState.Loading<*> -> startLoading()
               }
           }
       }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM