Search code examples
fluttervalidationbloc

removing errors from EventSink in flutter


I get the weird behavior from the flutter app, trying to fix this for two days...

  1. If I try to submit the login form, right after hot restart (empty fields) -> error: fields are empty -> then I fill the form, which I will try to resubmit again and everything works fine.
  2. If I do hot restart and fill the login form without, prior empty form submission, the submit button will never send the data to the server. Due to existing EventSink errors, which for some reason do not show up in the first case.

Here is the code: Form widgets:

@override
  Widget build(BuildContext context) {
    final FormBloc formBloc = FormProvider.of(context);

    Widget _emailField(FormBloc formBloc) {
      return StreamBuilder(
          stream: formBloc.email,
          builder: (context, AsyncSnapshot<String> snapshot) {
            return TextFormField(
              autofocus: false,
              keyboardType: TextInputType.emailAddress,
              onChanged: formBloc.changeEmail,


   Widget _passwordField(FormBloc formBloc) {
      return StreamBuilder(
          stream: formBloc.password,
          builder: (context, AsyncSnapshot<String> snapshot) {
            return TextFormField(
                autofocus: false,
                obscureText: !_passwordVisible,
                keyboardType: TextInputType.text,
                onChanged: formBloc.changePassword,

Formbloc:

class FormBloc with LoginValidationMixin {
  final _email = BehaviorSubject<String>();
  final _password = BehaviorSubject<String>();
  final _errorMessage = BehaviorSubject<String>();

  // getters
  Function(String) get changeEmail => _email.sink.add;
  Function(String) get changePassword => _password.sink.add;
  Function(String) get addError => _errorMessage.sink.add;

  // streams
  Stream<String> get email => _email.stream.transform(validatorEmail);
  Stream<String> get password => _password.stream.transform(validatorPassword);
  Stream<String> get errorMessage => _errorMessage.stream;

  Stream<bool> get submitValidForm => Rx.combineLatest3(
        email,
        password,
        errorMessage,
        (e, p, er) => true,
      );

  late AuthService authInfo;

  // login
  dynamic login(BuildContext context) async {
    authInfo = AuthService();

    final email = _email.valueOrNull;
    final password = _password.valueOrNull;

    if (email == null || password == null) {
      addError("Password or Email cannot be empty");
      return;
    }

    final response =
        await authInfo.login(email, password, this);
    if (response == null) {return;}
    final data = jsonDecode(response) as Map<String, dynamic>;

    if (data['status_code'] != 200) {
    } else {
      final tokens = data['tokens'];

      AuthService.setToken(
          tokens["access"], tokens["refresh"], data['client_id']);
      Navigator.pushNamed(context, '/home');
      return data;
    }
  }

Validation


class LoginValidationMixin {
  static String get passwordPattern => "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}";

  final validatorEmail = StreamTransformer<String, String>.fromHandlers(
    handleData: (email, sink) {
      sink.add(email);
      if (!EmailValidator.validate(email)) {
        sink.addError('Email should look like:'
            'welcome@itsy.ext\n');
      } else {
        sink.add(email);
      }
    },
  );

  final validatorPassword = StreamTransformer<String, String>.fromHandlers(
    handleData: (password, sink) {
      sink.add(password);
      if (!RegExp(passwordPattern).hasMatch(password)) {
        sink.addError('A hint, password should contain\n'
            'at least 8 characters\n'
            'at least 1 upper case character\n'
            'at least 1 upper case character\n'
            'at least 1 numerical character\n');
      } else {
        sink.add(password);
      }
    },
  );
}

Login Button

Widget _loginButton(FormBloc formBloc) {
      return StreamBuilder<bool>(
          stream: formBloc.submitValidForm,
          builder: (context, AsyncSnapshot<bool> snapshot) {
            return Material(
              child: MaterialButton(
                onPressed: () {
                  if (snapshot.error != null) {
                    print(snapshot.error)  <<<< in the second scenario, here is always an error, even if filed validations have passed. While in the first scenario, this does work as expected.
                    return;
                  } else {
                    formBloc.login(context);
                  }
                },

So, in the second case, EventSink always holds an error and button is basically not clickable. However, in the first case EventSink works fine, holds errors if there are some and clears them if correct values are added. What should I do? Is there a way to clear EventSink errors?


Solution

  • So this is how I resolved the case, first I realized that streams will only yield value and not store.

    pskink: EvenSink does not hold an error Next, I checked that

    Stream<bool> get submitValidForm => Rx.combineLatest3(
            email,
            password,
            errorMessage, <<< this one is null in the case two, therefore my solution was to give it an initial value while handling email or password.
            (e, p, er) => true,
          );