Search code examples
androidkotlinandroid-fragmentsandroid-jetpackandroid-architecture-navigation

Navigation Component Fragment stuck running last lines of code after 1-st execution


I have 1 activity with 3 fragments: Sign In/Sign Up/Forgot Password

Sign In, which is the main fragment among these 3 works just fine, however, I have the issue when working with Sign Up/Forgot Password fragments.

Currently I have this navigation component:

my nav component

Sign In Fragment:

class SignInFragment : Fragment() {

    private val loginViewModel: LoginViewModel by activityViewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_sign_in, container, false)

        // see "ProgressButton" in dependencies of build.gradle file.
        // used for more interactive button.
        bindProgressButton(view.button_sign_in_submit)

        loginViewModel.signInState.observe(viewLifecycleOwner, { state ->
            when (state) {
                is UserSignInState.Loading -> view.button_sign_in_submit.showProgress {
                    buttonTextRes = R.string.status_loading
                    progressColor = Color.WHITE
                }
                is UserSignInState.SignedIn -> {
                    view.button_sign_in_submit.hideProgress(R.string.button_sign_in_submit)

                    startActivity(Intent(this.activity, ProfileActivity::class.java))
                    this.activity?.finish()
                }
                is UserSignInState.Failure -> {
                    view.button_sign_in_submit.hideProgress(R.string.button_sign_in_submit)
                    view.sign_in_input_password_layout.error = state.exception.localizedMessage
                }
            }
        })

        // Used for resetting focus and hiding keyboard for better user experience.
        view.sign_in_input_email.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
        view.sign_in_input_password.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }

        view.button_go_to_sign_up.setOnClickListener {
            hideKeyboard()
            this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
        }

        view.button_go_to_forgot_password.setOnClickListener {
            hideKeyboard()
            this.findNavController().navigate(R.id.action_signInFragment_to_forgotPasswordFragment)
        }

        view.button_sign_in_submit.setOnClickListener {
            hideKeyboard()

            view.sign_in_input_email_layout.error = null
            view.sign_in_input_password_layout.error = null

            val email = view.sign_in_input_email.text.toString()
            val password = view.sign_in_input_password.text.toString()

            if (email.isNotEmpty() && password.isNotEmpty()) {
                if (isEmailValid(email)) {
                    loginViewModel.userSignIn(email, password)
                } else {
                    view.sign_in_input_email_layout.error = getString(R.string.error_message_email_is_not_valid)
                }
            } else {
                view.sign_in_input_password_layout.error = getString(R.string.error_message_please_fill_in_all_fields)
            }
        }
        return view
    }
}

ForgotPassword Fragment:

class ForgotPasswordFragment : Fragment() {

    private val loginViewModel: LoginViewModel by activityViewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_forgot_password, container, false)
        // see "ProgressButton" in dependencies of build.gradle file.
        // used for more interactive button.
        bindProgressButton(view.button_forgot_password_submit)

        this.retainInstance = false

        loginViewModel.passwordResetState.observe(viewLifecycleOwner, { state ->
            when (state) {
                is UserPasswordResetState.Loading -> view.button_forgot_password_submit.showProgress {
                    buttonTextRes = R.string.status_loading
                    progressColor = Color.WHITE
                }
                is UserPasswordResetState.Sent -> {
                    view.button_forgot_password_submit.hideProgress(R.string.button_forgot_password_submit)
                    alertDialogSuccess(requireContext(), getString(R.string.alert_dialog_message_forgot_password_email_has_been_sent))
                    activity?.findNavController(R.id.nav_host_fragment_login)?.navigate(R.id.action_forgotPasswordFragment_to_signInFragment)
                }
                is UserPasswordResetState.Error -> {
                    view.button_forgot_password_submit.hideProgress(R.string.button_forgot_password_submit)

                    // Could be possibly replaced with implementation of "alertDialogFailure"
                    view.forgot_password_input_field_email_layout.error = state.exception.localizedMessage
                }
            }

        })

        view.forgot_password_input_field_email.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }

        view.button_forgot_password_submit.setOnClickListener {
            hideKeyboard()

            if (isEmailValid(view.forgot_password_input_field_email.text.toString())) {
                loginViewModel.userResetPassword(view.forgot_password_input_field_email.text.toString())
            } else if (!isEmailValid(view.forgot_password_input_field_email.text.toString())) {
                view.forgot_password_input_field_email_layout.error = getString(R.string.error_message_email_is_not_valid)
            } else {
                view.forgot_password_input_field_email_layout.error = getString(R.string.error_message_unknown)
            }
        }
        return view
    }
}

Navigation XML

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph_login"
    app:startDestination="@id/signInFragment">

    <fragment
        android:id="@+id/signInFragment"
        android:name="my.test.movieexpert._login.view.fragments.SignInFragment"
        android:label="fragment_sign_in"
        tools:layout="@layout/fragment_sign_in">
        <action
            android:id="@+id/action_signInFragment_to_signUpFragment"
            app:destination="@id/signUpFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim" />
        <action
            android:id="@+id/action_signInFragment_to_forgotPasswordFragment"
            app:destination="@id/forgotPasswordFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim" />
    </fragment>
    <fragment
        android:id="@+id/signUpFragment"
        android:name="my.test.movieexpert._login.view.fragments.SignUpFragment"
        android:label="fragment_sign_up"
        tools:layout="@layout/fragment_sign_up">
        <action
            android:id="@+id/action_signUpFragment_to_signInFragment"
            app:destination="@id/signInFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popUpTo="@+id/signInFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/forgotPasswordFragment"
        android:name="my.test.movieexpert._login.view.fragments.ForgotPasswordFragment"
        android:label="fragment_forgot_password"
        tools:layout="@layout/fragment_forgot_password">
        <action
            android:id="@+id/action_forgotPasswordFragment_to_signInFragment"
            app:destination="@id/signInFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popUpTo="@+id/signInFragment"
            app:popUpToInclusive="true" />
    </fragment>
</navigation>

What happens is: I launch my app. Press - Forgot Password, then enter my e-mail, I get alert dialog that the e-mail has been sent and then I am transferred to my sign-in screen. However, when I next time press "Forgot Password" I do not see that same field and etc, instead all I get is that AlertDialog that was run last time with Success message. (Yet I don't receive the message) so the Fragment gets stuck and I can never again get inside unless I restart the app.

I have no idea what is the cause of this and how to fix this, please help me out here.


Solution

  • The problem lies here:

    private val loginViewModel: LoginViewModel by activityViewModels()
    

    by activityViewModels() makes it so that the state persists between all Fragments that are referencing the ViewModel in such manner.

    Simply replace activityViewModels() with viewModels()