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.
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.
"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?
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) }
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