How to set error on EditText using DataBinding Framework MVVM

前端 未结 4 1843
旧巷少年郎
旧巷少年郎 2021-02-19 09:26

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



        
相关标签:
4条回答
  • 2021-02-19 09:58

    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.

    0 讨论(0)
  • 2021-02-19 10:01

    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.

    0 讨论(0)
  • 2021-02-19 10:01

    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) {
            }
        }
    
    0 讨论(0)
  • 2021-02-19 10:05

    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"
                    />
    
    0 讨论(0)
提交回复
热议问题