1

I'm working with the MVVM standard recently and need to validate the fields of a form when the user clicks the submit button. Example form:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="user" 
            type="me.example.presentation.model.User" />
        <variable
            name="presenter"
            type="me.example.presentation.view.LoginActivity"/>

... <!-- some code -->

    <EditText
        android:id="@+id/etPassword"
        android:layout_width="match_parent"                           
        android:layout_height="wrap_content"                          
        android:digits="@string/allowed_digits_vehicle_plate"                   
        android:hint="@string/login_hint_vehicle_plate"
        android:inputType="textFilter|textCapCharacters"
        android:maxLength="7"
        android:text="@={user.password}"
        password="@{user.password}"
        android:textSize="@dimen/size16" />

... <!-- some code -->

    <Button
        android:id="@+id/btEnter"
        android:layout_width="match_parent"
        android:layout_height="@dimen/login_button_enter"
        android:layout_marginTop="@dimen/margin_16dp"
        android:layout_marginBottom="@dimen/margin_8dp"
        android:text="@string/enter"
        android:onClick="@{() -> presenter.onLoginClick()}"/>

... <!-- some code -->


I'm trying to validate edittext using a BindingAdapter as below:

@JvmStatic
@BindingAdapter("password")
fun setPassError(editText: EditText, pass: String) {
    if (pass.isEmpty()) {
       editText.error = null
       return
    }

    if (editText.text.toString().length  < 7) {
       editText.error = "invalid pass"
    } else {
       editText.error = null
    }
}

That way it validates while the user is typing, but I want it to perform validation when the submit button is clicked. How can I change and improve this approach?

ruitercomp
  • 162
  • 2
  • 16

5 Answers5

2

you can add text watcher to your edit text then save your edit text in a string field in your model then use it on button click.

class Model{
   private TextWatcher textWatcher;
   private String text;

   public Model() {
       this.textWatcher = new TextChangeWatcher() {
           @Override
           public void onTextChanged(CharSequence s, int start, int before, int count) {
               text= s.toString();
           }
       };

   }

   public void btnClick() {
    //now you can validate string text here
   }
}
<Button
        android:id="@+id/btEnter"
        android:layout_width="match_parent"
        android:layout_height="@dimen/login_button_enter"
        android:layout_marginTop="@dimen/margin_16dp"
        android:layout_marginBottom="@dimen/margin_8dp"
        android:text="@string/enter"
        android:onClick="@{() -> model.btnClick()}"/>

for adding text watcher to edit text you can use How to databind to onTextChanged for an EditText on Android? this answer.

mahdi shahbazi
  • 1,882
  • 10
  • 19
2

Personally I'd keep the 2 way binding like you have and the click handling. I wouldn't bother with a binding adapter because the databinding library can figure out the method setError.

On the XML I'd assign the error to an observable that could be in the presenter variable (I know you said mvvm, but the XML calls it presenter).

<EditText
    app:error="presenter.passwordError"
    ...
    />

And now inside the click listener you can add the validation logic

val passwordError = ObservableField<String>()

fun btnClick() {
  if (user.pass.isEmpty()) {
    passwordError.set("Empty password")
    return
  }

  if (user.pass.length  < 7) {
    passwordError.set( "nvalid pass")
  } else {
    passwordError.set(null)
  }
}

I'm assuming you can access the user variable from the presenter. Nevertheless, I think this is enough to get the idea.

Fred
  • 16,367
  • 6
  • 50
  • 65
  • Thanks for the answer this approach seems interesting to me, in fact I think there are several ways to be done. Yesterday I implemented an approach I will post it here. – ruitercomp Apr 25 '19 at 11:31
1

I did this way to check the edittext field:

// I created a LiveData to observe the validations inside my ViewModel.
private val validationLiveEvent = LiveEvent<Validations>()
val validation: LiveData<Validations> = validationLiveEvent

fun validate (user: User) : Boolean {

   if (user.email.trim { it <= ' ' }.isEmpty() 
       || user.email.trim { it <= ' ' }.length < 6) {

       // I put the value so that the screen has some action
       validationLiveEvent.value = Validations.EmailEmpty
       return false
   }

   if (user.password.trim { it <= ' ' }.isEmpty()) {
       validationLiveEvent.value = Validations.PasswordEmpty
       return false
   }

   return true
}

In my Activity I pass the User object to my ViewModel like this:

fun onLoginClick() {
    binding.user?.let { loginViewModel.onLoginClick(it) }
}

Observing the validation also within the Activity I can say which message should appear on the screen:

loginViewModel.validation.observe(this, Observer {

    when(it) {
        Validations.EmailEmpty -> {
            binding.etEmail.error = getString(R.string.login_hint_email_error)
            binding.etEmail.focus()
        }

        Validations.PasswordEmpty -> {
            binding.tilPassword.isPasswordVisibilityToggleEnabled = false
            binding.etPassword.error = getString(R.string.login_password_hint)
            binding.etPassword.focus()
        }
    }
})

I think there are several ways to be done feel free to post more answers. I liked all approaches and will test them all. Tks!

ruitercomp
  • 162
  • 2
  • 16
1

I have written my own builder in order to construct a LiveData which depends on some given constraints and other LiveData instances to use as triggers. See below.

/**
 * Builder class used to create {@link LiveData} instances which combines multiple
 * sources (triggers) and evaluates given constraints to finally emit
 * either {@code true} (success/valid) or {@code false} (fail/invalid) which is an
 * aggregation of all the constraints using the {@code AND} operator.
 */
public final class ValidatorLiveDataBuilder {

    /**
     * A boolean supplier which supports aggregation of suppliers using {@code AND} operation.
     */
    private static final class BooleanAndSupplier implements BooleanSupplier {

        /**
         * Field for the source {@code supplier}.
         */
        private final BooleanSupplier source;

        /**
         * Private constructor
         *
         * @param source source to base this supplier on
         */
        private BooleanAndSupplier(BooleanSupplier source) {
            this.source = source;
        }

        /**
         * Returns a new {@code supplier} which combines {@code this} instance
         * and the given supplier. <br />
         * <b>Note:</b> the given {@code supplier} is not called if {@code this} instance
         * evaluates to {@code false}.
         *
         * @param supplier the supplier to combine with
         * @return a new combined {@code BooleanAndSupplier}
         */
        private BooleanAndSupplier andThen(BooleanSupplier supplier) {
            return new BooleanAndSupplier(() -> {
                if (!getAsBoolean()) {
                    return false;
                }

                return supplier.getAsBoolean();
            });
        }

        @Override
        public boolean getAsBoolean() {
            return source.getAsBoolean();
        }
    }

/**
 * Field for the returned {@link LiveData}.
 */
private final MediatorLiveData<Boolean> validatorLiveData = new MediatorLiveData<>();

/**
 * Field for the used validator.
 */
private BooleanAndSupplier validator = new BooleanAndSupplier(() -> true);

/**
 * Field for all the added sources.
 */
private final List<LiveData<?>> sources = new ArrayList<>();

/**
 * Constructor
 */
private ValidatorLiveDataBuilder() {
    // empty
}

/**
 * Constructs a new {@code ValidatorLiveDataBuilder}.
 *
 * @return new instance
 */
public static ValidatorLiveDataBuilder newInstance() {
    return new ValidatorLiveDataBuilder();
}

/**
 * Adds a source to {@code this} builder which is used as a trigger to evaluate the
 * added constraints.
 *
 * @param source the source to add
 * @return this instance to allow chaining
 */
public ValidatorLiveDataBuilder addSource(LiveData<?> source) {
    sources.add(source);
    return this;
}

/**
 * Adds a constraint to {@code this} builder which is evaluated when any of the added
 * sources emits value and aggregated using the {@code && (AND)} operator.
 *
 * @param constraint the constraint to add
 * @return this instance to allow chaining
 */
public ValidatorLiveDataBuilder addConstraint(BooleanSupplier constraint) {
    validator = validator.andThen(constraint);
    return this;
}

/**
 * Adds a source to {@code this} builder which is used as a trigger to evaluate
 * the added constraints. The given {@code constraint} gets the current item
 * in the {@code source} when any of the added sources emits a value. <br />
 *
 * <b>Note:</b> the item given to the constraint might be {@code null}.
 *
 * @param source     source to add
 * @param constraint the constraint to add
 * @param <T>        type of the items emitted by the source
 * @return this instance to allow chaining
 */
public <T> ValidatorLiveDataBuilder addSource(LiveData<T> source,
                                              Function<T, Boolean> constraint) {
    return addSource(source)
            .addConstraint(() -> constraint.apply(source.getValue()));
}

/**
 * Constructs a {@code LiveData} from {@code this} builder instance which
 * is updated to the result of the constraints when any of the sources emits a value. <br />
 * <b>Note:</b> a synthetic emission is forced in order to prevent cases where
 * none of the given sources has emitted any data and the validation logic is not run
 * on the first subscription. In other words, the validation logic will always evaluate
 * directly on subscription (observation).
 *
 * @return live data instance
 */
public LiveData<Boolean> build() {

    // Creates the observer which is called when any of the added sources
    // emits a value. The observer checks with the added constraints and updates
    // the live data accordingly.
    Observer<Object> onChanged = o -> validatorLiveData.setValue(validator.getAsBoolean());

    // Adds all the added sources to this live data with the same observer.
    for (LiveData<?> source : sources) {
        validatorLiveData.addSource(source, onChanged);
    }

    // Forces a validation call on first subscription.
    onChanged.onChanged(null);

    return validatorLiveData;
}
}

I use this with a Command type (they idea copied from .NET WPF).

public interface Command<T> {

    void execute(T arg);

    boolean canExecute(T arg);
}

These two work perfectly when combined using the following BindingAdapter.

@BindingAdapter(value = {"command", "command_arg", "command_validator"}, requireAll = false)
public static <T> void setCommand(View view, Command<T> command, T arg, Boolean valid) {
    boolean enabled = true;

    if (command != null && !command.canExecute(arg)) {
        enabled = false;
    }

    if (valid != null && !valid) {
        enabled = false;
    }

    if (view.isEnabled() ^ enabled) {
        // Enables or disables the view if they two are different (XOR).
        view.setEnabled(enabled);
    }

    if (command != null) {
        view.setOnClickListener(v -> command.execute(arg));
    }
}

Example of usage

Goal to allow the press of the button when the EditText contains any data and when the Command is executing, to disallow presses again.

Nothin in the EditText (initially)

enter image description here

Input 100 inside the EditText, UI validation is OK

enter image description here

The request is pending

enter image description here

We can accomplish the following by first constructing a command instance which we allow for binding by the view.

private Command<Object> requestBalanceCommand 
        = Commands.newInstance(this::requestBalance, this::canRequestBalance);

@Bindable
public Command<Object> getRequestBalanceCommand() {
    return requestBalanceCommand;
}

  public boolean canRequestBalance(Object ignored) {
    return isInState(State.STANDBY);
}

public void requestBalance(Object ignored) {
    setState(State.REQUESTING);

    if (balanceAmount.getValue() == null) {
        event.setValue(Event.FAILED_TO_SEND);
        return;
    }

    Timber.e("Requesting %d balance...", balanceAmount.getValue());

    Disposable disposable = Completable.timer(3, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(() -> {
                Timber.e("DONE!");
                setState(State.STANDBY);
            });

    addDisposable(disposable);

}

(The isInState() and setState() are just two methods on this view model to set the current state. The setState also notifies that the Bindable command has been updated using:

notifyPropertyChanged(BR.requestBalanceCommand)

You need to implement androidx.databinding.Observable on your ViewModel to allow for this, the information to do this can be found in the documentation.)

(The Commands class is just a static factory which creates a Command instance, see below INCOMPLETE snippet for the idea how to implement it.)

public static <T> Command<T> newInstance(Consumer<T> execute, Predicate<T> canExecute) {
    return new CommandImpl<>(execute, canExecute);
}

(The CommandImpl implements Command simply holds the Consumer and Predicate which it delegates to. But you could just as well return an anonymous class just implementing the Command interface right there in the static factory.)

And we construct the LiveData used for validation.

validator = ValidatorLiveDataBuilder.newInstance()
            .addSource(edtLiveData, amount -> {
                Timber.e("Checking amount(%d) != null = %b", amount, amount != null);
                return amount != null;
            })
            .build();

And expose it like so.

private final LiveData validator;

public LiveData<Boolean> getValidator() {
    return validator;
}

(The edtLiveData is a MutableLiveData instance hooked to the text of the EditText in question using two-way data binding.)

Now we attach it using the BindingAdapter on to the button.

<Button command="@{vm.requestBalanceCommand}"
        command_validator="@{vm.validator}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Request Balance" />

Please comment if anything is unclear, requires more code or is missing. The hooking of the EditText to the LiveData requires a Converter and InverseMethod but I didn't want to get into that part in this post, I assumed the LiveData to the EditText is already working. I know this might be more than what OP is looking for but I know I appreciate more complete examples than just small crumbs here and there.

What we achieved

Combined validation with command execution in a clean and stable way which makes sense and is easy to use.

Ludvig W
  • 744
  • 8
  • 27
1

you shouldn't This is the way you can validate data of EditText from ViewModel XML Layout

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

    <data>

        <variable
            name="loginViewModel"
            type="com.xxx.android.ui.customer.login.LoginViewModel" />

        <variable
            name="customerViewModel"
            type="com.xxx.android.ui.customer.CustomerViewModel" />

    </data>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <LinearLayout
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/edtEmail"
            android:layout_marginTop="40dp"
            android:layout_marginStart="24dp"
            android:layout_marginEnd="24dp"
            android:textSize="14sp"
            android:inputType="textEmailAddress"
            android:hint="Enter email"
            android:onTextChanged="@{loginViewModel.onEmailChanged}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/edtPassword"
            android:layout_marginStart="24dp"
            android:layout_marginEnd="24dp"
            android:layout_marginTop="12dp"
            android:textSize="14sp"
            android:inputType="textPassword"
            android:hint="Enter password"
            android:onTextChanged="@{loginViewModel.onPasswordChanged}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>


        <androidx.appcompat.widget.AppCompatButton
            android:layout_marginTop="24dp"
            android:background="@android:color/holo_orange_dark"
            android:textColor="@color/white"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:enabled="@{ loginViewModel.isEmailValidate &amp; loginViewModel.isPasswordValidate() ? true : false  }"

            android:padding="12dp"
            android:text="@string/logout" />



    </LinearLayout>

    </androidx.core.widget.NestedScrollView>
</layout>

And here is the way you validate in ViewModel

private var _isEmailValidate = MutableLiveData<Boolean>()
val isEmailValidate: LiveData<Boolean>
get() = _isEmailValidate

private var _isPasswordValidate = MutableLiveData<Boolean>()
val isPasswordValidate: LiveData<Boolean>
get() = _isPasswordValidate

fun onEmailChanged(s: CharSequence, start: Int, before: Int, count: Int) {
    val email = s.toString()
    _isEmailValidate.postValue(android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches())
    Log.e("Frank", "onEmailChanged ${s.toString()}")

}

fun onPasswordChanged(s: CharSequence, start: Int, before: Int, count: Int) {
    Log.e("Frank", "onEmailChanged ${s.toString()}")
    val result = (s.length > 0)
    _isPasswordValidate.postValue(result)
}

Please take not that you must type "android:onTextChanged", Android Studio will not suggest for you.

NhatVM
  • 1,964
  • 17
  • 25
  • this method can't generate a specific error text, you don't have context here in viewmodel, so it just says `true` or `false` which is not enough, for one field it can be minimum three case: empty, length, regex issue, so three different texts are needed – user924 Aug 10 '21 at 11:40
  • 1
    You can create a data class : ValidateInput(isValid:Boolean, message: String). Then create a variable in ViewModel : emailValidateInput = MutableLiveData(ValidateInput). Whenever onTextChange, you can check the text and update this value. The view in xml file will observer this variable of ViewModel to show warning message. – NhatVM Aug 11 '21 at 07:23
  • For me it's clear approach – Adrian Grygutis Mar 21 '22 at 15:05