I am using Android Data Binding framework I have suppose an EditText for login form with username as below
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:
<EditView
android:text="@={viewModel.email}"
android:error="@={viewModel.emailRule.check(email)} />
Data Binding ensures that check
function is invoked whenever email
is changed.
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";
}
}));
}
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.
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.
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) && 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) {
}
}
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"
/>