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
  }


}