How to set error on EditText using DataBinding Fra

2019-04-21 17:13发布

问题:

I am using Android Data Binding framework I have suppose an EditText for login form with username as below

<EditText
        android:id="@+id/etext_uname"
        style="@style/login_edittext"
        android:hint="@string/hint_username"
        android:inputType="textEmailAddress" />

I have defined LoginViewModel also but I need help how to set Error in edittext when user type wrong email address in some event let say inside

public void afterTextChanged(@NonNull final Editable editable)

Because as far as I know in Traditional Android approach we can do this programmatically via et.setError() method but I don't want to create edittext object via Activity or Fragment.

回答1:

If you want to do something like EditText.setError() function with databinding, here is two method.

Method 1

Used the final EditText view generated from the data binding (https://developer.android.com/topic/libraries/data-binding/index.html#views_with_ids)

You can call the EditText directly without creating it manually since it is automatically generated after you set the id for the view (also true for the included layout) .

MainActivityBinding.etext_uname.setError("Wrong email format");

Or

MainActivityBinding.etext_uname.addTextChangedListener(new MyOwnTextWatcher());

Method 2

If you want to use the binding method with xml as George mentioned (https://medium.com/google-developers/android-data-binding-custom-setters-55a25a7aea47#.su88ujqrn)

First you have to set your own binding method. Suggest to create another class for all the binding method.

Method must be static, with @BindingAdapter annotation and the corresponding binding method name (Namespace and the method name can be customized)

1. Set the Custom TextWatcher

public class MyOwnBindingUtil {
    public interface StringRule {
        public boolean validate(Editable s);
    }
    @BindingAdapter("android:watcher")
    public static void bindTextWatcher(EditText pEditText, TextWatcher pTextWatcher) {
        pEditText.addTextChangedListener(pTextWatcher);
    }
    @BindingAdapter(value = {"email:rule", "email:errorMsg"}, requireAll = true)
    public static void bindTextChange(final EditText pEditText, final StringRule pStringRule, final String msg) {
        pEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }
            @Override
            public void afterTextChanged(Editable s) {
                if (!pStringRule.validate(s)) {
                    pEditText.setError(msg);
                }
            }
        });
    }
    /*
    Your other custom binding method
     */
}

If you want to setup your own TextWatcher with custom action, like Toast shown, Dialog shown. You should use "android:watcher" method

mBinding.setWatcher(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }
    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }
    @Override
    public void afterTextChanged(Editable s) {
    }
});

In xml,

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

    <data>
        <variable
            name="watcher"
            type="android.text.TextWatcher"/>
        <variable
            name="emailRule"
            type="example.com.testerapplication.MyOwnBindingUtil.StringRule"/>
        <variable
            name="errorMsg"
            type="java.lang.String"/>
    </data>
    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Input Email"
        android:watcher="@{watcher}
        />

2. Setup your own validation Rule and error Msg

If you want to use setError function and only left the errorMsg and validation logic to be customized. You can set the xml like the following.

In xml,

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

    <data>
        <variable
            name="watcher"
            type="android.text.TextWatcher"/>
        <variable
            name="emailRule"
            type="example.com.testerapplication.MyOwnBindingUtil.StringRule"/>
        <variable
            name="errorMsg"
            type="java.lang.String"/>
    </data>
    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Input Email"
        email:rule="@{emailRule}"
        email:errorMsg="@{errorMsg}"
        />

Activity code

mBinding.setErrorMsg("Wrong type");
mBinding.setEmailRule(new MyOwnBindingUtil.StringRule() {
    @Override
    public boolean validate(Editable s) {
        // check if the length of string is larger than 18  
        return s.toString().length() > 18;
    }
});

Please feel free to edit my code to make the binding be more generic for the developer use.



回答2:

Fundamentally, you need a way to implement dependent fields. Error is dependent on the value of text. You want error value to get updated when text changes.

I have found two ways to achieve this:

Set attribute using Data Binding expression

<EditView
    android:text="@={viewModel.email}"
    android:error="@={viewModel.emailRule.check(email)} />

Data Binding ensures that check function is invoked whenever email is changed.

Use RxJava to convert from one field to another

I have written a utility to convert between ObservableField and Observable. See FieldUtils.java

Using this, you can implement in your ViewModel/Model code.

public class ViewModel {
    ObservableField<String> email = new ObservableField<>();
    ObservableField<String> emailError = toField(toObservable(email).map(new Func1<String, String>() {
            @Override
            public String call(String email) {
                return FormUtils.checkEmail(email) ? null : "Invalid Email";
            }
        }));
}

Problem with EditText

EditText clears the error when user types. Data Binding expects that attribute's value is retained after invoking setter. So, it does not invoke the setter again if the value doesn't change. Hence, as soon as you type, if the computed error value is same, data binding will not call setter and hence, the error will disappear. This kind of makes error attribute incompatible with Data Binding.

I prefer to use TextInputLayout provided by design library. It has a persistent error field and also looks better.



回答3:

I just want to share my modification of the answer of Long Ranger for android arch viewModel:

    public class StringValidationRules {

    public static StringRule NOT_EMPTY = new StringRule() {
        @Override
        public boolean validate(Editable s) {
            return TextUtils.isEmpty(s.toString());
        }
    };

    public static StringRule EMAIL = new StringRule() {
        @Override
        public boolean validate(Editable s) {
            return !android.util.Patterns.EMAIL_ADDRESS.matcher(s).matches();

        }
    };

    public static StringRule PASSWORD = new StringRule() {
        @Override
        public boolean validate(Editable s) {
            return s.length() < 8;
        }
    };

    public interface StringRule {
        boolean validate(Editable s);
    }
}

the viewModel...

    public class LoginViewModel extends ViewModel {
...
@BindingAdapter({"app:validation", "app:errorMsg"})
    public static void setErrorEnable(EditText editText, StringValidationRules.StringRule stringRule, final String errorMsg) {
        editText.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) {
                if (stringRule.validate(editText.getText())) {
                    editText.setError(errorMsg);
                } else {
                    editText.setError(null);
                }
            }
        });
    }

...

and the 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"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    >
    <data>
        <variable name="viewModel" type="com.fernandonovoa.sapmaterialstockoverview.login.LoginViewModel"/>
        <import type="com.fernandonovoa.sapmaterialstockoverview.utils.StringValidationRules" />
    </data>

...

<EditText
                android:id="@+id/etEmail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Ingrese su email"
                android:inputType="textEmailAddress"
                android:drawableLeft="@drawable/ic_email"
                android:drawableStart="@drawable/ic_email"
                app:validation="@{StringValidationRules.EMAIL}"
                app:errorMsg='@{"Email no válido"}'
                style="@style/AppTheme.Widget.TextInputLayoutLogin"
                />

<EditText
                android:id="@+id/etPassword"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Ingrese su contraseña"
                android:inputType="textPassword"
                android:drawableLeft="@drawable/ic_lock"
                android:drawableStart="@drawable/ic_lock"
                app:validation="@{StringValidationRules.PASSWORD}"
                app:errorMsg='@{"Contraseña no válida"}'
                style="@style/AppTheme.Widget.TextInputLayoutLogin"
                />


回答4:

You can also add validation on edit text like this.

Layout file

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.example.app.ui.login.LoginViewModel" />

        <import type="com.example.app.ui.ValidationRule" />

        <variable
            name="watcher"
            type="android.text.TextWatcher" />

        <import type="com.example.app.utils.ValidationUtils" />

    </data>

    <RelativeLayout
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp"
        tools:context=".ui.login.LoginFragment">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:orientation="vertical">

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="username"
                android:watcher="@{watcher}"
                app:error="@{@string/validation_error_msg_email}"
                app:rule="@{ValidationRule.EMPTY}">

                <com.google.android.material.textfield.TextInputEditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@={viewModel.usernameObs}" />
            </com.google.android.material.textfield.TextInputLayout>


            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="password"
                android:watcher="@{watcher}"
                app:error="@{@string/validation_error_msg_password}"
                app:rule="@{ValidationRule.PASSWORD}">

                <com.google.android.material.textfield.TextInputEditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="textPassword"
                    android:text="@={viewModel.passwordObs}" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.button.MaterialButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="16dp"
                android:background="?colorAccent"
                android:enabled="@{ValidationUtils.isValidEmail(viewModel.usernameObs) &amp;&amp; ValidationUtils.isValidPassword(viewModel.passwordObs)}"
                android:onClick="@{() -> viewModel.login()}"
                android:text="Login"
                android:textColor="?android:textColorPrimaryInverse" />
        </LinearLayout>
    </RelativeLayout>
</layout>

BindingUtils

object BindingUtils {
        @BindingAdapter(value = ["error", "rule", "android:watcher"], requireAll = true)
        @JvmStatic
        fun watcher(textInputLayout: com.google.android.material.textfield.TextInputLayout, errorMsg: String, rule: ValidationRule, watcher: TextWatcher) {
            textInputLayout.editText?.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(p0: Editable?) {
                }

                override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                }

                override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                    textInputLayout.error = null
                    if (rule == ValidationRule.EMPTY && !ValidationUtils.isValidEmail(p0.toString())) textInputLayout.error = errorMsg
                    if (rule == ValidationRule.PASSWORD && !ValidationUtils.isValidPassword(p0.toString())) textInputLayout.error = errorMsg
                }
            })
        }
    }

ValidationRule

enum class ValidationRule{
    EMPTY, EMAIL, PASSWORD
}

Don't forget to set the watcher in fragment or activity like this

binding.watcher = object : TextWatcher {
        override fun afterTextChanged(p0: Editable?) {
        }

        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }

        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }
    }