Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJava2 form validation

I have a form with 4 possible options that need to be checked (could be less as well depending on circumstances). There are 2 editexts, one for email and one for a reference field when creating an order.

The email and reference fields may or may not be left empty based on conditions (which are available when the form is created). Additionally we may need to show an alert dialog to tell the user that it may not be possible to show the reference value (to the recipient of the order) and they may also need to agree to a terms and condition alert dialog.

Currently the onConfirm checks is something like this,

void onCreateOrderConfirmed(@Nullable final String receiverEmail,
                            @Nullable final String reference,
                            @Nullable final Boolean noRefAgreed,
                            @Nullable final Boolean termsAndConditionsAgreed) {

    if (!reviewCompletionState.emailRequirementSatisfied()) {
        if (!isValidEmail(receiverEmail)) {
            view.showEmailError();
            return;
        }

        reviewCompletionState = reviewCompletionState.newBuilder()
                .receiverEmail(receiverEmail)
                .emailRequirementSatisfied(true)
                .build();
    }

    if (!reviewCompletionState.referenceRequirementSatisfied()) {
        if (isEmpty(reference)) {
            view.showReferenceError();
            return;
        }

        reviewCompletionState = reviewCompletionState.newBuilder()
                .reference(reference)
                .referenceRequirementSatisfied(true)
                .build();
    }

    if (!reviewCompletionState.noRefAgreed()) {
        if (noRefAgreed == null || !noRefAgreed) {
            view.showNoReferenceAlert();
            return;
        }

        reviewCompletionState = reviewCompletionState.newBuilder()
                .noRefAgreed(true)
                .build();
    }

    if (!reviewCompletionState.termsAndConditionsAgreed()) {
        if (termsAndConditionsAgreed == null || !termsAndConditionsAgreed) {
            view.showTermsDisclaimerAlert();
            return;
        }

        reviewCompletionState = reviewCompletionState.newBuilder()
                .termsAndConditionsAgreed(true)
                .build();
    }

    createOrder();
}

I would love to know if there is a way to make this validation simpler with RxJava2? (but don't currently know enough to be able to do this)

TIA

like image 209
source.rar Avatar asked Nov 10 '17 09:11

source.rar


2 Answers

This can be a simple. There will be a lot of code, I'll show the result first.

    private ReviewValidator reviewValidator = new ReviewValidator();

    void onCreateOrderConfirmed(@Nullable final String receiverEmail,
                                @Nullable final String reference,
                                @Nullable final Boolean noRefAgreed,
                                @Nullable final Boolean termsAndConditionsAgreed) {
        ReviewState reviewState = new ReviewState(receiverEmail,
                reference,
                noRefAgreed,
                termsAndConditionsAgreed);//another model for simplicity

        reviewValidator.validate(reviewState)
                .flatMap(reviewState -> /* create order */)
                .subscribe(this::onOrderCreated, this::onOrderCreatingError);

    }

    void onOrderCreated(Object order) {//or what you need here
        //handle positive result
    }

    void onOrderCreatingError(Throwable throwable) {
        if (throwable instanceof ValidateException) {
            List<ValidateError> errors = ((ValidateException) throwable).getValidateErrors();
            for (ValidateError error: errors) {
                switch (error.getField()) {
                    case EMAIL: {
                        view.showEmailError();
                        return;//or break if you want show all errors
                    }
                    case REFERENCE: {
                        view.showReferenceError();
                        return;
                    }
                    //handle another errors....
                }
            }
        //handle another error cases...
    }

First, create model for reviewState:

public class ReviewState {

    private String receiverEmail;
    private String reference;
    private Boolean noRefAgreed;
    private Boolean termsAndConditionsAgree;

    public ReviewState(String receiverEmail,
                       String reference,
                       Boolean noRefAgreed,
                       Boolean termsAndConditionsAgree) {
        this.receiverEmail = receiverEmail;
        this.reference = reference;
        this.noRefAgreed = noRefAgreed;
        this.termsAndConditionsAgree = termsAndConditionsAgree;
    }

    public String getReceiverEmail() {
        return receiverEmail;
    }

    public String getReference() {
        return reference;
    }

    public Boolean getNoRefAgreed() {
        return noRefAgreed;
    }

    public Boolean getTermsAndConditionsAgree() {
        return termsAndConditionsAgree;
    }
}

Then create you own validator. It is not necessary to create a whole model, you can create validator for every field and and link them with flatMap(), your choice.

public class ReviewValidator extends Validator<ReviewState> {

    @Override
    protected List<ValidateFunction> getValidateFunctions(ReviewState reviewState) {
        List<ValidateFunction> validateFunctions = new LinkedList<>();
        validateFunctions.add(() -> validateEmail(reviewState.getReceiverEmail()));
        validateFunctions.add(() -> validateReference(reviewState.getReference()));
        //another validation methods
        return validateFunctions;
    }

    private ValidateError validateEmail(String email) {
        if (TextUtils.isEmpty(email)) {
            return new ValidateError(Field.EMAIL);//Field.EMAIL - just enum
        }
        return null;
    }


    private ValidateError validateReference(String reference) {
        if (TextUtils.isEmpty(reference)) {
            return new ValidateError(Field.REFERENCE);
        }
        return null;
    }
    //....
    //another validation methods
}

Abstract class for validator:

public abstract class Validator<Model> {

    public Single<Model> validate(Model model) {
        return Single.just(model)
                .map(this::validateModel)
                .flatMap(this::processResult);
    }

    private Single<Model> processResult(ValidateResultModel<Model> validateResultModel) {
        return Single.create(subscriber -> {
            List<ValidateError> validateErrors = validateResultModel.getValidateErrors();
            if (validateErrors.isEmpty()) {
                subscriber.onSuccess(validateResultModel.getModel());
            } else {
                subscriber.onError(new ValidateException(validateErrors));
            }
        });
    }

    private ValidateResultModel<Model> validateModel(Model model) {
        List<ValidateError> errors = new LinkedList<>();
        for (ValidateFunction validateFunctions : getValidateFunctions(model)) {
            ValidateError error = validateFunctions.validate();
            if (error != null) {
                errors.add(error);
            }
        }
        return new ValidateResultModel<>(model, errors);
    }

    protected abstract List<ValidateFunction> getValidateFunctions(Model model);

    protected interface ValidateFunction {

        @Nullable
        ValidateError validate();
    }
}

Helper classes for validator...

public class ValidateError {

    private Field field;

    public ValidateError(Field field) {
        this.field = field;
    }

    public Field getField() {
        return field;
    }
}

class ValidateResultModel<T> {

    private T model;
    private List<ValidateError> validateErrors;

    ValidateResultModel(T model, List<ValidateError> validateErrors) {
        this.model = model;
        this.validateErrors = validateErrors;
    }

    T getModel() {
        return model;
    }

    List<ValidateError> getValidateErrors() {
        return validateErrors;
    }
}

public class ValidateException extends RuntimeException {

    private List<ValidateError> validateErrors;

    ValidateException(List<ValidateError> validateErrors) {
        this.validateErrors = validateErrors;
    }

    public List<ValidateError> getValidateErrors() {
        return validateErrors;
    }
}

Initially, I took the idea from here: https://github.com/matzuk/TestableCodeMobius/tree/master/app/src/main/java/com/matsyuk/testablecodemobius/business/transfer/validation

like image 143
Anrimian Avatar answered Oct 06 '22 17:10

Anrimian


i think you should the RxJava CombineLatest, so you need all of the form input are producing an observable, then you just combine it and adjust the view

as a reference you can check:

https://medium.com/@etiennelawlor/rxjava-on-the-sign-in-screen-9ecb66b88572

Using RxJava for email login validation, an observable is emitting twice

========

example:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxCompoundButton;
import com.jakewharton.rxbinding2.widget.RxTextView;

import io.reactivex.Observable;


public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    EditText receiverText = findViewById(R.id.input_receiver);
    EditText referenceText = findViewById(R.id.input_reference);
    CheckBox checkRef = findViewById(R.id.check_ref);
    CheckBox checkTerms = findViewById(R.id.check_terms);
    Button buttonLogin = findViewById(R.id.button_login);

    Observable<CharSequence> receiverObservable = RxTextView.textChanges(receiverText).skip(1); // can add more logic
    Observable<CharSequence> referenceObservable = RxTextView.textChanges(referenceText).skip(1); // can add more logic
    Observable<Boolean> refCheckObservable = RxCompoundButton.checkedChanges(checkRef); // can add more logic
    Observable<Boolean> termsCheckObservable = RxCompoundButton.checkedChanges(checkTerms); // can add more logic

    Observable<String> combineObservable = Observable.combineLatest(
            receiverObservable,
            referenceObservable,
            refCheckObservable,
            termsCheckObservable, (receiverCharSequence, referenceCharSequence, refBoolean, termsBoolean) -> {
                // add logic here for now it is only combine the input
                return  receiverCharSequence + " " + referenceCharSequence + " " + refBoolean + " " + termsBoolean ;}
            );

    RxView.clicks(buttonLogin).flatMap(o -> { return combineObservable;}).distinctUntilChanged().subscribe(string -> {
        Toast.makeText(this, string, Toast.LENGTH_LONG).show();
    });

    }
}
like image 22
Bobby Prabowo Avatar answered Oct 06 '22 18:10

Bobby Prabowo