简体   繁体   中英

Data Binding and: Input field validation and manipulation; Activity/Fragment Navigation

I am currently learning data binding and all the new things that come with it. At the moment I am struggling quite a bit with how to properly implement things, so asking for some help.

In this particular part of my application we will talk about what I experience most problems with: SignIn/SignUp forms;

In the past, my code would be simple:

  • I do things in my ViewModel & Repository
  • LiveData is being observed inside Fragment or Activity
  • Based on the LiveData state, UI changes.

And everyone is happy while keeping things simple.

At this moment, I trying to transfer to Data Binding and while I have managed to understand quite a bit about, there are some things I am not quite sure about.

Current code and specific question below:

SignInViewModel:

class SignInViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext
    val signInForm = SignInForm()

    private val _signInState = MutableLiveData<SignInState>()
    val signInState: LiveData<SignInState>
        get() = _signInState

    fun userSignIn() {
        _signInState.value = SignInState.Loading

        Firebase.auth.signInWithEmailAndPassword(signInForm.email!!.value!!, signInForm.password!!.value!!)
            .addOnCompleteListener {
                _signInState.value =
                    if (it.isSuccessful)
                        SignInState.SignedIn
                    else
                        SignInState.Error(it.exception!!.localizedMessage!!)
            }
    }


    // E-Mail
    val emailValidationResponse = MediatorLiveData<String?>().apply {
        addSource(signInForm.email as LiveData<String>) {
            value = emailValidation()
        }
    }

    val passwordValidationResponse = MediatorLiveData<String?>().apply {
        addSource(signInForm.password as LiveData<String>) {
            value = passwordValidation()
        }
    }

    private fun emailValidation(): String? {
        return when {
            signInForm.email?.value.isNullOrEmpty() -> {
                context.getString(R.string.error_message_field_is_empty)
            }
            !Patterns.EMAIL_ADDRESS.matcher(signInForm.email?.value!!).matches() -> {
                context.getString(R.string.error_message_invalid_email)
            }
            else -> null
        }
    }

    private fun passwordValidation(): String? {
        return when {
            signInForm.password?.value.isNullOrEmpty() -> {
                context.getString(R.string.error_message_field_is_empty)
            }
            signInForm.password?.value!!.length < 8 -> {
                context.getString(R.string.error_message_password_is_too_short, USER_PASSWORD_MIN_CHARACTERS)
            }
            else -> null
        }
    }

SignInForm.kt

data class SignInForm(
    override val email: MutableLiveData<String>? = MutableLiveData(),
    val password: MutableLiveData<String>? = MutableLiveData()
) : Form()

SignInState.kt

sealed class SignInState {
    object Loading : SignInState()
    object SignedIn : SignInState()
    data class Error(val errorMessage: String) : SignInState()
}

SignInFragment

class SignInFragment : Fragment() {

    private val signInViewModel: SignInViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val binding = FragmentSignInBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = signInViewModel

        signInViewModel.signInState.observe(viewLifecycleOwner, { state ->
            when (state) {
                is SignInState.SignedIn -> {
                    proceedToProfileScreen(requireActivity())
                }
            }
        })

        binding.signInInputEditTextEmail.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
        binding.signInInputEditTextPassword.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }

        binding.signInButtonGoToSignUp.setOnClickListener {
            this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
        }

        binding.signInButtonGoToForgotPassword.setOnClickListener {
            this.findNavController().navigate(R.id.action_signInFragment_to_forgotPasswordFragment)
        }

        return binding.root
    }
}

fragment_sign_in.xml

<layout> ....

<com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/sign_in_input_layout_email"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    setError="@{viewModel.emailValidationResponse}"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_hint_email"
                    app:errorEnabled="true">

                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/sign_in_input_edit_text_email"
                        android:layout_width="match_parent"
                        android:layout_height="55dp"
                        android:inputType="text"
                        android:text="@={viewModel.signInForm.email}" />

                </com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/sign_in_input_layout_password"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    setError="@{viewModel.passwordValidationResponse}"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_hint_password"
                    app:endIconMode="password_toggle"
                    app:errorEnabled="true">

                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/sign_in_input_edit_text_password"
                        android:layout_width="match_parent"
                        android:layout_height="55dp"
                        android:inputType="textPassword"
                        android:text="@={viewModel.signInForm.password}" />

                </com.google.android.material.textfield.TextInputLayout>


<androidx.appcompat.widget.AppCompatButton
                    android:id="@+id/sign_in_button_submit"
                    style="@style/Widget.MaterialComponents.Button.OutlinedButton"
                    android:layout_width="match_parent"
                    android:layout_height="55dp"
                    android:onClick="@{() -> viewModel.userSignIn()}"
                    android:text="@string/sign_in_button_submit"
                    android:textColor="@android:color/black" />

<androidx.appcompat.widget.AppCompatButton
                    android:id="@+id/sign_in_button_go_to_sign_up"
                    style="@style/Widget.MaterialComponents.Button.TextButton"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/sign_in_button_go_to_sign_up"
                    android:textColor="@android:color/black"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />

                <androidx.appcompat.widget.AppCompatButton
                    android:id="@+id/sign_in_button_go_to_forgot_password"
                    style="@style/Widget.MaterialComponents.Button.TextButton"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/sign_in_button_go_to_forgot_password"
                    android:textColor="@android:color/black"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
... </layout>

Questions:

  • Is this the correct way of using Data Binding to work with UI?
  • Is there a better way of using Navigation Component to navigate between Fragments with Data Binding
  • What would be a good solution to enable/disable sign in button based on E-Mail and Password input? Custom Binding Adapter or another variable in ViewModel?

Here is what I ended up using:

[1] Input Form Validation + [3] Enabling/Disabling button.

SignInForm.kt

data class SignInForm(
    val email: MutableLiveData<String> = MutableLiveData(),
    val password: MutableLiveData<String> = MutableLiveData()
) {

    val emailError = MediatorLiveData<String?>().apply {
        value = ""
        addSource(email) {
            value = validateEmail(it)
        }
    }

    val passwordError = MediatorLiveData<String?>().apply {
        value = ""
        addSource(password) {
            value = validatePassword(it)
        }
    }
}

InputValidators.kt

fun validateEmail(email: String?): String? {
    return when {
        email.isNullOrEmpty() -> TheContext.applicationContext().getString(R.string.error_message_field_is_empty)
        !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() -> TheContext.applicationContext().getString(R.string.error_message_invalid_email)
        else -> null
    }
}

fun validatePassword(password: String?): String? {
    return when {
        password.isNullOrEmpty() -> TheContext.applicationContext().getString(R.string.error_message_field_is_empty)
        password.length < 8 -> TheContext.applicationContext().getString(R.string.error_message_password_is_too_short, USER_PASSWORD_MIN_CHARACTERS)
        else -> null
    }
}

fragment_sign_in.xml

<layout> ...

<data>
        <variable
            name="viewModel"
            type="my.test.movieexpert.loginscreen.viewmodel.SignInViewModel" />
</data>



... <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/sign_in_input_layout_email"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    inputValidation="@{viewModel.signInForm.emailError}"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_hint_email"
                    app:errorEnabled="true"> ...



... <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/sign_in_input_layout_password"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    inputValidation="@{viewModel.signInForm.passwordError}"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_hint_password"
                    app:endIconMode="password_toggle"
                    app:errorEnabled="true"
                    app:helperText=""> ...

... <androidx.appcompat.widget.AppCompatButton
                    android:id="@+id/sign_in_button_submit"
                    emailError="@{viewModel.signInForm.emailError}"
                    passwordError="@{viewModel.signInForm.passwordError}"
                    setButtonState="@{viewModel.signInState}"
                    android:layout_width="match_parent"
                    android:layout_height="55dp"
                    android:background="@color/colorPrimaryDark"
                    android:onClick="@{() -> viewModel.userSignIn()}"
                    android:text="@string/sign_in_button_submit"
                    android:textColor="@android:color/white"
                    android:textSize="17sp" /> ...

... </layout>

[2] Navigation stayed the same with:

SignInFragment

binding.signInButtonGoToSignUp.setOnClickListener {
            this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
        }

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