Can i bind an error message to a TextInputLayout?

define your xml like this

 <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/emailTextInputLayout"
            style="@style/myTextInputLayoutStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="80dp"
            android:layout_marginEnd="16dp"
            **app:errorEnabled="true"**
            **app:errorText="@{viewModel.emailErrorMessage}"**
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/include">

            <com.google.android.material.textfield.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/email"
                android:inputType="textEmailAddress"
                **android:text="@={viewModel.emailText}" />**
  </com.google.android.material.textfield.TextInputLayout>

then put this method anywhere

@BindingAdapter("app:errorText")
fun setErrorText(view: TextInputLayout, errorMessage: String) {
    if (errorMessage.isEmpty())
        view.error = null
    else
        view.error = errorMessage;
}

let's say you will make your validation after clicking on button so your button will be like this

   <Button
        android:id="@+id/signInButtonView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="68dp"
        android:layout_marginEnd="16dp"
        **android:onClick="@{() -> viewModel.logIn()}"**
        android:text="@string/sign_in"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/passwordTextInputLayout" />

then inside your view model

you will be having these

class SignInViewModel : ViewModel() {
  private val _emailErrorMessage = MutableLiveData("")
    val emailText = MutableLiveData("")    

    val emailErrorMessage: LiveData<String> = _emailErrorMessage

    fun logIn() {
        if (validateInput()) {

        }
    }

    private fun validateInput(): Boolean {
        if (emailText.value?.length!! < 5) {
            _emailErrorMessage.value = "no way"
            return false
        }
        _emailErrorMessage.value = ""
        return true

    }
.
.

and don't forget to add this in your activity or fragment

    binding.lifecycleOwner = this

code is long so I added double asterisk on important lines


I have made a binding like my answer on How to set error on EditText using DataBinding Framwork MVVM. But this time it used TextInputLayout as sample, like the previous one.

Purposes of this idea:

  1. Make the xml as readable as possible and independent
  2. Make the activity-side validation and xml-side validation independently

Of course, you can make you own validation and set it using the <variable> tag in xml

First, implements the static binding method and the related String validation rules for preparation.

Binding

  @BindingAdapter({"app:validation", "app:errorMsg"})
  public static void setErrorEnable(TextInputLayout textInputLayout, StringRule stringRule,
      final String errorMsg) {
  }

StringRule

  public static class Rule {

    public static StringRule NOT_EMPTY_RULE = s -> TextUtils.isEmpty(s.toString());
    public static StringRule EMAIL_RULE = s -> s.toString().length() > 18;
  }

  public interface StringRule {

    boolean validate(Editable s);
  }

Second, put the default validation and error message in the TextInputLayout and make it easy to know the validation, binding in xml

<android.support.design.widget.TextInputLayout
      android:id="@+id/imageUrlValidation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:validation="@{Rule.NOT_EMPTY_RULE}"
      app:errorMsg='@{"Cannot be empty"}'
      >
      <android.support.design.widget.TextInputEditText
        android:id="@+id/input_imageUrl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Image Url"
        android:text="@={feedEntry.imageUrl}" />
    </android.support.design.widget.TextInputLayout>

Thirdly, when the click button trigger, you can use the predefined id in TextInputLayout(e.g. imageUrlValidation) to do the final validation on activity

Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
        button.setOnClickListener(view1 -> {
          // TODO Do something
          //to trigger auto error enable
          FeedEntry inputFeedEntry = dialogFeedEntryBinding.getFeedEntry();
          Boolean[] validations = new Boolean[]{
              dialogFeedEntryBinding.imageUrlValidation.isErrorEnabled(),
              dialogFeedEntryBinding.titleValidation.isErrorEnabled(),
              dialogFeedEntryBinding.subTitleValidation.isErrorEnabled()
          };
          boolean isValid = true;
          for (Boolean validation : validations) {
            if (validation) {
              isValid = false;
            }
          }
          if (isValid) {
            new AsyncTask<FeedEntry, Void, Void>() {
              @Override
              protected Void doInBackground(FeedEntry... feedEntries) {
                viewModel.insert(feedEntries);
                return null;
              }
            }.execute(inputFeedEntry);
            dialogInterface.dismiss();
          }
        });

The complete code is following:

dialog_feedentry.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <data>

    <import type="com.example.common.components.TextInputEditTextBindingUtil.Rule" />

    <variable
      name="feedEntry"
      type="com.example.feedentry.repository.bean.FeedEntry" />

  </data>
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"
    android:orientation="vertical">
    <android.support.design.widget.TextInputLayout
      android:id="@+id/imageUrlValidation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:validation="@{Rule.NOT_EMPTY_RULE}"
      app:errorMsg='@{"Cannot be empty"}'
      >
      <android.support.design.widget.TextInputEditText
        android:id="@+id/input_imageUrl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Image Url"
        android:text="@={feedEntry.imageUrl}" />
    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
      android:id="@+id/titleValidation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:validation="@{Rule.NOT_EMPTY_RULE}"
      app:errorMsg='@{"Cannot be empty"}'
      >

      <android.support.design.widget.TextInputEditText
        android:id="@+id/input_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Title"
        android:text="@={feedEntry.title}"

        />
    </android.support.design.widget.TextInputLayout>
    <android.support.design.widget.TextInputLayout
      android:id="@+id/subTitleValidation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:validation="@{Rule.NOT_EMPTY_RULE}"
      app:errorMsg='@{"Cannot be empty"}'
      >

      <android.support.design.widget.TextInputEditText
        android:id="@+id/input_subtitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Subtitle"
        android:text="@={feedEntry.subTitle}"

        />
    </android.support.design.widget.TextInputLayout>
  </LinearLayout>
</layout>

TextInputEditTextBindingUtil.java

package com.example.common.components;

import android.databinding.BindingAdapter;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;

/**
 * Created by Charles Ng on 7/9/2017.
 */

public class TextInputEditTextBindingUtil {


  @BindingAdapter({"app:validation", "app:errorMsg"})
  public static void setErrorEnable(TextInputLayout textInputLayout, StringRule stringRule,
      final String errorMsg) {
    textInputLayout.getEditText().addTextChangedListener(new TextWatcher() {
      @Override
      public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      }

      @Override
      public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      }

      @Override
      public void afterTextChanged(Editable editable) {
        textInputLayout
            .setErrorEnabled(stringRule.validate(textInputLayout.getEditText().getText()));
        if (stringRule.validate(textInputLayout.getEditText().getText())) {
          textInputLayout.setError(errorMsg);
        } else {
          textInputLayout.setError(null);
        }
      }
    });
    textInputLayout
        .setErrorEnabled(stringRule.validate(textInputLayout.getEditText().getText()));
    if (stringRule.validate(textInputLayout.getEditText().getText())) {
      textInputLayout.setError(errorMsg);
    } else {
      textInputLayout.setError(null);
    }
  }

  public static class Rule {

    public static StringRule NOT_EMPTY_RULE = s -> TextUtils.isEmpty(s.toString());
    public static StringRule EMAIL_RULE = s -> s.toString().length() > 18;
  }

  public interface StringRule {

    boolean validate(Editable s);
  }

}

FeedActivity.java

public class FeedActivity extends AppCompatActivity {

  private FeedEntryListViewModel viewModel;

  @SuppressLint("StaticFieldLeak")
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_feed);
    viewModel = ViewModelProviders.of(this)
        .get(FeedEntryListViewModel.class);
    viewModel.init(this);
    ViewPager viewPager = findViewById(R.id.viewpager);
    setupViewPager(viewPager);
    // Set Tabs inside Toolbar
    TabLayout tabs = findViewById(R.id.tabs);
    tabs.setupWithViewPager(viewPager);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    FloatingActionButton fab = findViewById(R.id.fab);
    fab.setOnClickListener(view -> {
      //insert sample data by button click
      final DialogFeedentryBinding dialogFeedEntryBinding = DataBindingUtil
          .inflate(LayoutInflater.from(this), R.layout.dialog_feedentry, null, false);
      FeedEntry feedEntry = new FeedEntry("", "");
      feedEntry.setImageUrl("http://i.imgur.com/DvpvklR.png");
      dialogFeedEntryBinding.setFeedEntry(feedEntry);
      final Dialog dialog = new AlertDialog.Builder(FeedActivity.this)
          .setTitle("Create a new Feed Entry")
          .setView(dialogFeedEntryBinding.getRoot())
          .setPositiveButton("Submit", null)
          .setNegativeButton("Cancel", (dialogInterface, i) -> dialogInterface.dismiss())
          .create();
      dialog.setOnShowListener(dialogInterface -> {
        Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
        button.setOnClickListener(view1 -> {
          // TODO Do something
          //to trigger auto error enable
          FeedEntry inputFeedEntry = dialogFeedEntryBinding.getFeedEntry();
          Boolean[] validations = new Boolean[]{
              dialogFeedEntryBinding.imageUrlValidation.isErrorEnabled(),
              dialogFeedEntryBinding.titleValidation.isErrorEnabled(),
              dialogFeedEntryBinding.subTitleValidation.isErrorEnabled()
          };
          boolean isValid = true;
          for (Boolean validation : validations) {
            if (validation) {
              isValid = false;
            }
          }
          if (isValid) {
            new AsyncTask<FeedEntry, Void, Void>() {
              @Override
              protected Void doInBackground(FeedEntry... feedEntries) {
                viewModel.insert(feedEntries);
                return null;
              }
            }.execute(inputFeedEntry);
            dialogInterface.dismiss();
          }
        });
      });
      dialog.show();

    });
  }
}

As of writing this answer (May 2016), there is no XML attribute corresponding to setError() method, so you cannot set error message directly in your XML, which is bit odd knowing errorEnabled is there. But this ommision can be easily fixed by creating Binding Adapter that would fill the gap and provide missing implementation. Something like this:

@BindingAdapter("app:errorText")
public static void setErrorMessage(TextInputLayout view, String errorMessage) {
   view.setError(errorMessage);
}

See official binding docs, section "Attribute Setters" especially "Custom Setters".


EDIT

Possibly dumb question, but where should i put this? Do I need to extend TextInputLayout and put this in there?

It's actually not a dumb question at all, simply because you cannot get complete answer by reading the official documentation. Luckily it is pretty simple: you do not need to extend anything - just put that method anywhere in your projects. You can create separate class (i.e. DataBindingAdapters) or just add this method to any existing class in your project - it does not really matter. As long as you annotate this method with @BindingAdapter, and ensure it is public and static it does not matter what class it lives in.