Modern Android Form Validations With Data Binding

How to validate form fields without if or when expressions

Frameworks and Languages

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 1 like
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.

Get our stories delivered

From us to your inbox weekly.