Correct Form Validation using CombineLatest?

Question

When you are implementing a Form validation using Streams (and thus, in my case, a BLoC), there are cases where the outcome of the validation using Observable.combineLatest could give a wrong result.

This is due to the fact the streams, referenced by CombineLatest could have already emitted a positive validation, earlier.

As a reference, consider my initial article that explains the basics of such form validation process.

Answer

Before giving the solution to the problem, let’s first analyze it and understand why this might happen.

If I extract the part of the code that gives the outcome of the validation (and limit it to the notions of email and password to make the explanation a bit easier ), we have:

final BehaviorSubject<String> _emailController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordController = BehaviorSubject<String>();

//
//  Inputs
//
Function(String) get onEmailChanged => _emailController.sink.add;
Function(String) get onPasswordChanged => _passwordController.sink.add;

//
// Validators
//
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password => _passwordController.stream.transform(validatePassword);

//
// Outcome of the validation
//
Stream<bool> get registerValid => Observable.combineLatest2(
                                      email, 
                                      password, 
                                      (e, p) => true
                                    );

How does this work?

  • When the user enters the information related to the email, the input is sent to the _emailController via its sink (onEmailChanged)
  • The Stream emits the provided input to the StreamTransformer (validateEmail) which will perform the validation.
  • If the validation is positive, the input is sent back to the Stream (email)
  • If the validation is negative, an error is sent to the Stream (email)
  • The latter is listened by the Observable.combineLatest2, together with the other password output stream.
  • The same principle applies to the password.
  • When both email and password will have emitted something which is not an error, the Stream<bool> (registerValid) will emit a “true

So, theoretically, this should work but if you apply the following flow of actions:

  • enter a correct email => registerValid does not emit anything, yet
  • update the email and give an invalid one
  • enter a correct password => registerValid emits a positive answer

Since the email is invalid, this should not happen, will you tell me.

So why does this happen?

It happens because email stream has already emitted something after a positive validation, earlier .


How to solve this?

To solve this problem, I wrote the following class:

Here is the code that makes use of this class:

How does this work?

  • At initialization time of the RegistrationFormBloc (line #39), the ValidateAllStreamsHaveDataAndNoErrors class is instantiated and we also ask it to listen to the ‘email', ‘password’ and ‘confirmPassword’ streams.

  • Each time one of these streams will emit something (data or error), it will be intercepted by the ValidateAllStreamsHaveDataAndNoErrors class (lines #34-43)

  • The corresponding errors[index] will be set to false (case of emission of some data) or to true (case of emission of an error)

  • The _validate() method will be called, which will emit (via its own Stream) a positive answer if no errors still remain recorded (at the level of the errors[] array), otherwise it will emit a negative answer (= false).

  • This stream can now be safely used as the validation status (line #33 of the RegistrationFormBloc)


Conclusions

I suppose that there exist other solutions and maybe more elegant than this one, but at least this one works.

I hope that this can be useful in some developments.