Modern Android Form Validations With Data Binding
How to validate form fields without if or when expressions

Introduction
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 = <em>mutableListOf</em><Predicate>()
private val errorMessages = <em>mutableListOf</em><String>()
var error = MutableLiveData<String?>()
<strong>//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</strong>
fun isValid(): Boolean {
for (i in 0 <em>until </em>validationRules.size) {
if (validationRules[i](liveData.<em>value</em>)) {
emitErrorMessage(errorMessages[i])
return false
}
}
emitErrorMessage(null)
return true
}
<strong>//For emitting error messages</strong>
private fun emitErrorMessage(messageRes: String?) {
error.<em>value </em>= messageRes
}
<strong>//For adding validation rules</strong>
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).<em>apply </em><strong>{
//Whenever the condition of the predicate is true, the error message should be emitted
</strong>addRule("username is required") <strong>{ it</strong>.<em>isNullOrBlank</em>() <strong>}
}
</strong>val passwordLiveData = MutableLiveData<String>()
val passwordValidator = LiveDataValidator(passwordLiveData).<em>apply </em><strong>{
//We can add multiple rules.
//However the order in which they are added matters because the rules are checked one after the other
</strong>addRule("password is required") <strong>{ it</strong>.<em>isNullOrBlank</em>() <strong>}
</strong>addRule("password length must be 10") <strong>{ it?.length != 10</strong> <strong>}
}
//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
</strong>val isLoginFormValidMediator = MediatorLiveData<Boolean>()
init {
isLoginFormValidMediator.<em>value </em>= false
isLoginFormValidMediator.addSource(usernameLiveData) <strong>{ </strong>validateForm() <strong>}
</strong>isLoginFormValidMediator.addSource(passwordLiveData) <strong>{ </strong>validateForm() <strong>}</strong>
}
//This is called whenever the usernameLiveData and passwordLiveData changes
fun validateForm() {
val validators = <em>listOf</em>(usernameValidator, passwordValidator)
val validatorResolver = LiveDataValidatorResolver(validators)
isLoginFormValidMediator.<em>value </em>= 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"
<strong>app:error="@{viewModel.usernameValidator.error}"</strong>
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"
<strong>android:text="@={viewModel.usernameLiveData}"</strong> />
</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"
<strong>app:error="@{viewModel.passwordValidator.error}"</strong>
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"
<strong>android:text="@={viewModel.passwordLiveData}"</strong> />
</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"
<strong>android:enabled="@{viewModel.isLoginFormValidMediator}"</strong>
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>
<br>
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.


