0

I've a fragment making a network request based on the result, I'm navigating to the next fragment.

I am not able to go back to the previous fragment, this is the issue: https://streamable.com/4m2vzg

This is the code in the previous fragment

class EmailInputFragment :
    BaseFragment<FragmentEmailInputBinding>(FragmentEmailInputBinding::inflate) {

    private val viewModel by viewModels<EmailInputViewModel>()
    private lateinit var progressButton: ProgressButton

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.emailToolbar.setNavigationOnClickListener {
            val activity = activity as AuthActivity
            activity.onSupportNavigateUp()
        }
        binding.emailNextButton.pbTextview.text = getString(R.string.next)
        binding.emailNextButton.root.setOnClickListener {
            checkValidEmail()
        }
        binding.enterEmail.setOnEditorActionListener { _, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_DONE) {
                checkValidEmail()
            }
            false
        }
        binding.enterEmail.doAfterTextChanged {
            binding.enterEmailLayout.isErrorEnabled = false
        }
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.emailCheck.collect {
                when (it) {
                    State.Empty -> {
                    }
                    is State.Failed -> {
                        Timber.e(it.message)
                        progressButton.buttonFinished("Next")
                    }
                    State.Loading -> {
                        progressButton.buttonActivate("Loading")
                    }
                    is State.Success<*> -> {
                        it.data as EmailCheckModel
                        when (it.data.registered) {
                            // New User
                            0 -> {
                                findNavController().navigate(
                                    EmailInputFragmentDirections.actionEmailFragmentToSignupFragment(
                                        binding.enterEmail.text.toString().trim()
                                    )
                                )
                            }
                            // Existing User
                            1 -> {
                                findNavController().navigate(
                                    EmailInputFragmentDirections.actionEmailFragmentToPasswordInputFragment(
                                        binding.enterEmail.text.toString().trim()
                                    )
                                )
                            }
                            // Unverified user
                            2 -> {
                                findNavController().navigate(
                                    EmailInputFragmentDirections.actionEmailFragmentToVerifyUserFragment(
                                        "OK"
                                    )
                                )
                            }
                        }
                    }
                }
            }
        }
    }

    private fun checkValidEmail() {
        if (!binding.enterEmail.text.toString().trim().isValidEmail()) {
            binding.enterEmailLayout.error = "Please enter valid Email ID"
            return
        }
        progressButton = ProgressButton(requireContext(), binding.emailNextButton.root)
        viewModel.checkUser(binding.enterEmail.text.toString().trim())
    }
}

When I press back from the next fragment, as the state is still Success the flow is being collected and goes to next fragment, I've tried this.cancel to cancel the coroutine on create and still doesn't work. How do I go about this?

Moving the flow collect to the onClick of the button throws a error that navigation action cannot be found for the destination

I put a workaround of resetting the state of the flow back to State.EMPTY on success using

viewModel.resetState()

in onSuccess, I don't think this is the best way, any suggestions?

ViewModel code:

private val _emailCheckResponse = MutableStateFlow<State>(State.Empty)
val emailCheck: StateFlow<State> get() = _emailCheckResponse 
Abhishek AN
  • 648
  • 7
  • 24

2 Answers2

1

If your viewModel.emailCheck flow is a hot flow, then you need to manage its life cycle by yourself. If it is not a hot Flow, then you need to use LiveData to control the interface instead of simply collecting Flow. You should convert the flow to LiveData, and add the Observer to LiveData at the corresponding location.

There is no API related to the interface life cycle in Cold Flow, but the life cycle is already managed in LiveData.


viewModel.emailCheckLiveData.observe(viewLifecycleOwner, {
             when (it) {
                    State.Empty -> {
                    }
                    is State.Failed -> {
                        Timber.e(it.message)
                        progressButton.buttonFinished("Next")
                    }
                    State.Loading -> {
                        progressButton.buttonActivate("Loading")
                    }
                    is State.Success<*> -> {
                        it.data as EmailCheckModel
                        if (it.data.registered) {
                            val action =
                                EmailInputFragmentDirections.actionEmailFragmentToPasswordInputFragment(
                                    binding.enterEmail.text.toString().trim()
                                )
                            findNavController().navigate(action)
                        } else {
                            val action =
                                EmailInputFragmentDirections.actionEmailFragmentToSignupFragment(
                                    binding.enterEmail.text.toString().trim()
                                )
                            findNavController().navigate(action)
                        }
                    }
        })

You need to define emailCheckLiveData. In Flow.asLiveData()


    private val _emailCheckResponse = MutableStateFlow<State>(State.Empty)
    val emailCheck: StateFlow<State> get() = _emailCheckResponse

    private var mJob: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lifecycleScope.launchWhenResumed {
            if (mJob?.isActive == true) return
            mJob = _emailCheckResponse.collectLatest {
                when (it) {
                    State.Empty -> {
                    }
                    is State.Failed -> {
                        Timber.e(it.message)
                        progressButton.buttonFinished("Next")
                    }
                    State.Loading -> {
                        progressButton.buttonActivate("Loading")
                    }
                    is State.Success<*> -> {
                        it.data as EmailCheckModel
                        if (it.data.registered) {
                            val action =
                                EmailInputFragmentDirections.actionEmailFragmentToPasswordInputFragment(
                                    binding.enterEmail.text.toString().trim()
                                )
                            findNavController().navigate(action)
                        } else {
                            val action =
                                EmailInputFragmentDirections.actionEmailFragmentToSignupFragment(
                                    binding.enterEmail.text.toString().trim()
                                )
                            findNavController().navigate(action)
                        }
                    }
                }
            }
        }
    }

    override fun onDestroy() {
        mJob?.apply {
            if (isActive) cancel()
        }
        super.onDestroy()
    }

Future Deep Gone
  • 831
  • 6
  • 16
  • making it livedata with `.asLiveData()` fixed it, how do I manage the lifecycle of flow myself? – Abhishek AN May 26 '21 at 06:54
  • You don't need to do any management of Flow at this time. Put the code in Flow's collect into LiveData's listener, and LiveData will automatically manage its life cycle. – Future Deep Gone May 26 '21 at 07:03
  • Just a question, I wanted to eliminate completely the usage of LiveData is this the only way? – Abhishek AN May 26 '21 at 07:09
  • This is related to the flow you provide. If it is a Hot flow such as StatedFlow, then LiveData may not be used. In this case, you need to manage the life cycle yourself. – Future Deep Gone May 26 '21 at 07:13
  • I've used a stateflow like this in my VM, updated question with VM code `private val _emailCheckResponse = MutableStateFlow(State.Empty) val emailCheck: StateFlow get() = _emailCheckResponse ` And the result of retrofit with States.SUCCESS/FAILURE go into these flows – Abhishek AN May 26 '21 at 07:18
  • Oh well, using .collect{} instead of collectLatest worked – Abhishek AN May 26 '21 at 07:33
  • Okay, collectLatest is only used to process the latest value – Future Deep Gone May 26 '21 at 07:41
  • Okay collect{} was not working, i had a resetState function in the fragment that was changing in the viewmodel. – Abhishek AN May 28 '21 at 05:14
  • Your second snippet of code didn't work, I'm in a fragment btw – Abhishek AN May 28 '21 at 05:19
-2

After some time, stumbled on this article. https://proandroiddev.com/flow-livedata-what-are-they-best-use-case-lets-build-a-login-system-39315510666d

Scrolling down to the bottom gave the solution to my problem.

Abhishek AN
  • 648
  • 7
  • 24