Я написал свой собственный конструктор, чтобы создать 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 (изначально)
Ввод 100 внутри EditText, проверка пользовательского интерфейса в порядке
Запрос находится на рассмотрении
Сначала мы можем создать экземпляр командыкоторый мы допускаем для привязки представлением.
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, но я знаю, что ценю более полные примеры, чем просто маленькие крошки тут и там.
Что мы достигли
Комбинированная проверка с выполнением команды вчистый и стабильный способ, который имеет смысл и прост в использовании.