How to implement validation using ViewModel and Databinding?
There can be many ways to implement this. I am telling you two solutions, both works well, you can use which you find suitable for you.
I use extends BaseObservable
because I find that easy than converting all fields to Observers
. You can use ObservableFields
too.
Solution 1 (Using custom BindingAdapter
)
In xml
<variable name="model" type="sample.data.Model"/><EditText passwordValidator="@{model.password}" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={model.password}"/>
Model.java
public class Model extends BaseObservable { private String password; @Bindable public String getPassword() { return password; } public void setPassword(String password) { this.password = password; notifyPropertyChanged(BR.password); }}
DataBindingAdapter.java
public class DataBindingAdapter { @BindingAdapter("passwordValidator") public static void passwordValidator(EditText editText, String password) { // ignore infinite loops int minimumLength = 5; if (TextUtils.isEmpty(password)) { editText.setError(null); return; } if (editText.getText().toString().length() < minimumLength) { editText.setError("Password must be minimum " + minimumLength + " length"); } else editText.setError(null); }}
Solution 2 (Using custom afterTextChanged
)
In xml
<variable name="model" type="com.innovanathinklabs.sample.data.Model"/><variable name="handler" type="sample.activities.MainActivityHandler"/><EditText android:id="@+id/etPassword" android:layout_width="match_parent" android:layout_height="wrap_content" android:afterTextChanged="@{(edible)->handler.passwordValidator(edible)}" android:text="@={model.password}"/>
MainActivityHandler.java
public class MainActivityHandler { ActivityMainBinding binding; public void setBinding(ActivityMainBinding binding) { this.binding = binding; } public void passwordValidator(Editable editable) { if (binding.etPassword == null) return; int minimumLength = 5; if (!TextUtils.isEmpty(editable.toString()) && editable.length() < minimumLength) { binding.etPassword.setError("Password must be minimum " + minimumLength + " length"); } else { binding.etPassword.setError(null); } }}
MainActivity.java
public class MainActivity extends AppCompatActivity { ActivityMainBinding binding; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.setModel(new Model()); MainActivityHandler handler = new MainActivityHandler(); handler.setBinding(binding); binding.setHandler(handler); }}
Update
You can also replace
android:afterTextChanged="@{(edible)->handler.passwordValidator(edible)}"
with
android:afterTextChanged="@{handler::passwordValidator}"
Because parameter are same of android:afterTextChanged
and passwordValidator
.
This approach uses TextInputLayouts, a custom binding adapter, and creates an enum for form errors. The result I think reads nicely in the xml, and keeps all validation logic inside the ViewModel.
The ViewModel:
class SignUpViewModel() : ViewModel() { val name: MutableLiveData<String> = MutableLiveData() // the rest of your fields as normal val formErrors = ObservableArrayList<FormErrors>() fun isFormValid(): Boolean { formErrors.clear() if (name.value?.isNullOrEmpty()) { formErrors.add(FormErrors.MISSING_NAME) } // all the other validation you require return formErrors.isEmpty() } fun signUp() { auth.createUser(email.value!!, password.value!!) } enum class FormErrors { MISSING_NAME, INVALID_EMAIL, INVALID_PASSWORD, PASSWORDS_NOT_MATCHING, }}
The BindingAdapter:
@BindingAdapter("app:errorText")fun setErrorMessage(view: TextInputLayout, errorMessage: String) { view.error = errorMessage}
The XML:
<layout> <data> <import type="com.example.SignUpViewModel.FormErrors" /> <variable name="viewModel" type="com.example.SignUpViewModel" /> </data><!-- The rest of your layout file etc. --> <com.google.android.material.textfield.TextInputLayout android:id="@+id/text_input_name" android:layout_width="match_parent" android:layout_height="wrap_content" app:errorText='@{viewModel.formErrors.contains(FormErrors.MISSING_NAME) ? "Required" : ""}'> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Name" android:text="@={viewModel.name}"/> </com.google.android.material.textfield.TextInputLayout><!-- Any other fields as above format -->
And then, the ViewModel can be called from activity/fragment as below:
class YourActivity: AppCompatActivity() { val viewModel: SignUpViewModel // rest of class fun onFormSubmit() { if (viewModel.isFormValid()) { viewModel.signUp() // the rest of your logic to proceed to next screen etc. } // no need for else block if form invalid, as ViewModel, Observables // and databinding will take care of the UI }}
I've written a library for validating bindable fields of an Observable object.
Setup your Observable model:
class RegisterUser:BaseObservable(){@Bindablevar name:String?="" set(value) { field = value notifyPropertyChanged(BR.name) }@Bindablevar email:String?="" set(value) { field = value notifyPropertyChanged(BR.email) }
}
Instantiate and add rules
class RegisterViewModel : ViewModel() {var user:LiveData<RegisterUser> = MutableLiveData<RegisterUser>().also { it.value = RegisterUser()}var validator = ObservableValidator(user.value!!, BR::class.java).apply { addRule("name", ValidationFlags.FIELD_REQUIRED, "Enter your name") addRule("email", ValidationFlags.FIELD_REQUIRED, "Enter your email") addRule("email", ValidationFlags.FIELD_EMAIL, "Enter a valid email") addRule("age", ValidationFlags.FIELD_REQUIRED, "Enter your age (Underage or too old?)") addRule("age", ValidationFlags.FIELD_MIN, "You can't be underage!", limit = 18) addRule("age", ValidationFlags.FIELD_MAX, "You sure you're still alive?", limit = 100) addRule("password", ValidationFlags.FIELD_REQUIRED, "Enter your password") addRule("passwordConfirmation", ValidationFlags.FIELD_REQUIRED, "Enter password confirmation") addRule("passwordConfirmation", ValidationFlags.FIELD_MATCH, "Passwords don't match", "password")}
}
And setup your xml file:
<com.google.android.material.textfield.TextInputLayoutstyle="@style/textFieldOutlined"error='@{viewModel.validator.getValidation("email")}'android:layout_width="match_parent"android:layout_height="wrap_content"><com.google.android.material.textfield.TextInputEditText android:id="@+id/email" style="@style/myEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Your email" android:imeOptions="actionNext" android:inputType="textEmailAddress" android:text="@={viewModel.user.email}" />