How to validate form fields without if or when expressions
//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)
}
}
class LiveDataValidatorResolver(private val validators: List<LiveDataValidator>) {
fun isValid(): Boolean {
for (validator in validators) {
if (!validator.isValid()) return false
}
return true
}
}
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()
}
}
<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>
Looking for a new challenge? Join Our Team
From us to your inbox weekly.