Modern Android Form Validations With Data Binding

How to validate form fields without if or when expressions

Web & Mobile Development

imageIntroduction

One of the many things you need to know how to do as an android developer is creating forms. With that comes the responsibility of client side validation. There are various approaches used to do this but I'd like to share my personal favourite with you all.

Let's Get Into It

First we will be creating a validator class that will handle validation of liveData
//A predicate is a function that evaluates to true when its param matches the condition of the predicate
typealias Predicate = (value: String?) -> Boolean

class LiveDataValidator(private val liveData: LiveData<String>) {
    private val validationRules = mutableListOf<Predicate>()
    private val errorMessages = mutableListOf<String>()
    
     
     var error = MutableLiveData<String?>()

    //For checking if the liveData value matches the error condition set in the validation rule predicate
    //The livedata value is said to be valid when its value doesn't match an error condition set in the predicate
    fun isValid(): Boolean {
        for (i in 0 until validationRules.size) {
            if (validationRules[i](liveData.value)) {
                emitErrorMessage(errorMessages[i])
                return false
            }
        }

        emitErrorMessage(null)
        return true
    }

    //For emitting error messages
    private fun emitErrorMessage(messageRes: String?) {
        error.value = messageRes
    }

    //For adding validation rules
    fun addRule(errorMsg: String, predicate: Predicate) {
        validationRules.add(predicate)
        errorMessages.add(errorMsg)
    }
}


Then, we will create a ValidatorResolver which will help us aggregate multiple liveData validators. This will be useful when we want to check if all liveData attached to our form are in a valid state.

class LiveDataValidatorResolver(private val validators: List<LiveDataValidator>) {
    fun isValid(): Boolean {
        for (validator in validators) {
            if (!validator.isValid()) return false
        }
        return true
    }
}

Next, we will create the validation rules in our viewModel
class ExampleViewModel : ViewModel() {
   val usernameLiveData = MutableLiveData<String>()
   val usernameValidator = LiveDataValidator(usernameLiveData).apply {
      //Whenever the condition of the predicate is true, the error message should be emitted
      addRule("username is required") { it.isNullOrBlank() }
   }
   val passwordLiveData = MutableLiveData<String>()
   val passwordValidator = LiveDataValidator(passwordLiveData).apply {
      //We can add multiple rules. 
      //However the order in which they are added matters because the rules are checked one after the other
      addRule("password is required") { it.isNullOrBlank() }
      addRule("password length must be 10") { it?.length != 10 }
   }

   //We will use a mediator so we can update the error state of our form fields
   //and the enabled state of our login button as the form data changes
   val isLoginFormValidMediator = MediatorLiveData<Boolean>()

   init {
       isLoginFormValidMediator.value = false
       isLoginFormValidMediator.addSource(usernameLiveData) { validateForm() }
       isLoginFormValidMediator.addSource(passwordLiveData) { validateForm() }
   }

   //This is called whenever the usernameLiveData and passwordLiveData changes
   fun validateForm() {
       val validators = listOf(usernameValidator, passwordValidator)
       val validatorResolver = LiveDataValidatorResolver(validators)
       isLoginFormValidMediator.value = validatorResolver.isValid()
   }
}

Finally, we will be creating a simple login form containing two fields - username, password  - and a button to complete the login process. The XML below assumes you're familiar with data binding. 

<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/field_username"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:errorEnabled="true"
    app:error="@{viewModel.usernameValidator.error}"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@={viewModel.usernameLiveData}" />

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

<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/field_password"
    style="@style/TextInputLayoutTheme"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:errorEnabled="true"
    app:error="@{viewModel.passwordValidator.error}"
    app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toBottomOf="@+id/field_username">

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@={viewModel.passwordLiveData}" />

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

<com.google.android.material.button.MaterialButton
    android:id="@+id/button_log_in"
    android:layout_width="match_parent"
    android:layout_height="60dp"
     android:layout_marginTop="100dp"
    android:enabled="@{viewModel.isLoginFormValidMediator}"
    android:text="Log in"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/field_password"/>
</androidx.constraintlayout.widget.ConstraintLayout>


Conclusion

I prefer this method because it helps me avoid writing long if or when expressions whenever I have a huge form with plenty of validation rules. If you like flexing your creative muscles, you can give it a try in your next project and consider joining our team at Oozou.

Looking for a new challenge? Join Our Team

Like 10 likes
Anu Karounwi
Android engineer at oozou
Share:

Join the conversation

This will be shown public
All comments are moderated

Comments

Naser
December 21st, 2020
Thank you for sharing your knowledge. I really enjoyed reading this blog and I will surely implement this method in my projects. I’m just wondering why field_username has “ app:layout_constraintStart_toStartOf="match_parent" “ instead of “ app:layout_constraintStart_toStartOf="parent" “

Anu
December 21st, 2020
Thanks @Naser for pointing out the error, I've updated the post.

Emek
March 15th, 2021
Hello, something seems to be wrong for me, in the addRule function
when I add the condition {it.length < 7}
it requires me to add a null safety call to it(it.?length < 7)

something that you don't have in your code.

Rupam Das
May 27th, 2021
Everything was fine, except when I type something in the user name field, the password validator also responds and shows error message. This is bad, it should show error message only after focusing a particular field.

Anu
May 28th, 2021
Thanks @Emek, that was a typo. The post has been updated.

Tolga
September 28th, 2021
I agree with @Rupam Das 's mention.
"Everything was fine, except when I type something in the user name field, the password validator also responds and shows error message. This is bad, it should show error message only after focusing a particular field."

How can we set this to while user write the user name, I don't want to show password field throw errors. How can I handle this. Can you help me sir?

Anu
September 28th, 2021
@Rupam @Tolga, thank you for your comments. If you only want the field whose text was changed to be triggered, you could start off by updating the isValid method in LivedataValidator. You could end up with something like:
fun isValid(isEmitError: Boolean = true): Boolean {
for (i in 0 until validationRules.size) {
if (validationRules[i](liveData.value)) {
if (isEmitError) emitResErrorMessage(errorMessages[i])
return false
}
}

if (isEmitError) emitResErrorMessage(null)
return true
}

Your validator resolver could be refactored to be :
class LiveDataValidatorResolver(
private val validators: List,
private val isEmitFieldErrorList: List
) {
fun isValid(): Boolean {
for (i in validators.indices) {
val isEmitError = isEmitFieldErrorList[i]
if (!validators[i].isValid(isEmitError)) return false
}
return true
}
}

Finally, you can end up with a validateForm method that looks like this:
fun validateForm(
isUsernameChanged: Boolean = false,
isPasswordChanged: Boolean = false,
) {
val validators = listOf(usernameValidator, passwordValidator)
val isEmitFieldErrorList = listOf(isUsernameChanged, isPasswordChanged)
val validatorResolver = LiveDataValidatorResolver(validators, isEmitFieldErrorList)
isSignUpFormValidMediator.value = validatorResolver.isValid()
}

You can trigger the above method with :
isLoginFormValidMediator.addSource(usernameLiveData) { validateForm(isUsernameChanged = true) }
isLoginFormValidMediator.addSource(passwordLiveData) { validateForm(isPasswordChanged = true) }

Tolga
September 28th, 2021
@Anu You are awesome. This is what I was looking for. Thank you so much, appreciated.

Victor
April 4th, 2022
Hello, thank you for your wonderful code, but I encounter some problem.
When I add rule using isNullOrBlank() and reset my form using viewmodel.value = null, it will show the error message, which is a normal behavior since I put rule isNullOrBlank()
I want to make reset button, the function is more or less like this
fun resetForm(){
viewModel.value = null
viewModel2.value = null
viewModel3.value = null
}
is it possible to hide the error message when I hit the reset button?
I just want to make some exception if variable is null because I hit the reset button, then it not show the error message? Thank you in advance

Get our stories delivered

From us to your inbox weekly.