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)

Input 100 inside the EditText, UI validation is OK

The request is pending

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.