Как создать поле проверки формы с помощью MVVM + Databinding на Android - PullRequest
1 голос
/ 24 апреля 2019

Я недавно работаю со стандартом MVVM и мне нужно проверить поля формы, когда пользователь нажимает кнопку отправки. Пример формы:

<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 -->


Я пытаюсь проверить текст редактирования, используя BindingAdapter , как показано ниже:

@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
    }
}

Таким образом, он проверяет, когда пользователь печатает, но я хочу, чтобы он выполнял проверку при нажатии кнопки отправки. Как я могу изменить и улучшить этот подход?

Ответы [ 4 ]

2 голосов
/ 25 апреля 2019

Лично я бы оставил двухстороннее связывание, как у вас, и обработку кликов.Я не стал бы беспокоиться об адаптере привязки, потому что библиотека привязки данных может определить метод setError.

В XML я бы назначил ошибку наблюдаемой, которая может быть в переменной presenter (Я знаю, что вы сказали mvvm, но XML называет это презентатором)получить доступ к пользовательской переменной от докладчика.Тем не менее, я думаю, что этого достаточно, чтобы понять идею.

2 голосов
/ 24 апреля 2019

вы можете добавить наблюдатель текста к вашему тексту редактирования, затем сохранить текст редактирования в строковом поле в вашей модели и затем использовать его при нажатии кнопки.

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()}"/>

для добавления средства просмотра текста для редактирования текста вы можете использовать Как привязать данные к onTextChanged для EditText на Android? этот ответ.

1 голос
/ 24 июня 2019

Я написал свой собственный конструктор, чтобы создать LiveData, которая зависит от некоторых заданных ограничений и других экземпляров LiveData, которые будут использоваться в качестве триггеров.См. Ниже.

/**
 * 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;
}
}

Я использую это с типом Команды (по идее, они скопированы из .NET WPF).

public interface Command<T> {

    void execute(T arg);

    boolean canExecute(T arg);
}

Эти два прекрасно работают при объединении с помощью следующего 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));
    }
}

Пример использования

Цель , чтобы позволить нажатию кнопки, когда EditText содержит какие-либо данные и когда команда выполняется, запретить повторные нажатия.

Nothin в EditText (изначально)

enter image description here

Ввод 100 внутри EditText, проверка пользовательского интерфейса в порядке

enter image description here

Запрос находится на рассмотрении

enter image description here

Сначала мы можем создать экземпляр командыкоторый мы допускаем для привязки представлением.

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);

}

(isInState () и setState () - это всего лишь два метода в этой модели представления для установки текущего состояния. setState также уведомляет, что команда Bindable имеетбыл обновлен с помощью:

notifyPropertyChanged(BR.requestBalanceCommand)

Вам необходимо реализовать androidx.databinding.Observable на вашем ViewModel, чтобы разрешить это, информацию для этого можно найти в документации .)

(класс Commands - это просто статическая фабрика, которая создает экземпляр Commandсм. ниже фрагмент INCOMPLETE , чтобы узнать, как его реализовать.)

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

(CommandImpl реализует Command, просто храня Consumer и Predicate, которому она делегирует.Но вы также можете вернуть анонимный класс, просто реализуя интерфейс Command прямо в статической фабрике.)

И мы создадим LiveData, используемый для проверки.

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

И выставим еговот так.

приватный конечный валидатор LiveData;

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

(edtLiveData - это экземпляр MutableLiveData, подключенный к рассматриваемому тексту EditText с использованием двусторонней привязки данных.)

Теперь мы прикрепляем его с помощью BindingAdapter к кнопке.

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

Пожалуйста, прокомментируйте, если что-то неясно, требуется больше кода или отсутствует.Для подключения EditText к LiveData требуется конвертер и InverseMethod, но я не хотел вдаваться в эту часть в этом посте, я предположил, что LiveData для EditText уже работает.Я знаю, что это может быть больше, чем то, что ищет OP, но я знаю, что ценю более полные примеры, чем просто маленькие крошки тут и там.

Что мы достигли

Комбинированная проверка с выполнением команды вчистый и стабильный способ, который имеет смысл и прост в использовании.

1 голос
/ 25 апреля 2019

Я сделал так, чтобы проверить поле edittext:

// 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
}

В своей Деятельности я передаю объект User моей ViewModel следующим образом:

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

Наблюдая валидацию также в пределахЗадание Я могу сказать, какое сообщение должно появиться на экране:

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()
        }
    }
})

Я думаю, что есть несколько способов сделать это, не стесняйтесь размещать больше ответов.Мне понравились все подходы и опробую их все.Tks!

...