Search code examples
dartflutterblocrxdart

StreamBuilder TextField does not update its value when changed elsewhere


I have a reactive login form following the BLOC pattern. I'm trying to programmatically clear all the values in it. In my Bloc, my submit function passes empty strings to my stream sinks:

class Bloc with Validators {
  final _email = BehaviorSubject<String>();
  final _password = BehaviorSubject<String>();

  Stream<String> get email => _email.stream.transform(validateEmail);
  Stream<String> get password => _password.stream.transform(validatePassword);
  Stream<bool> get submitValid => Observable.combineLatest2(email, password, (String e, String p) {
    var valid = (e != null && e.isNotEmpty)
                && (p != null && p.isNotEmpty);
    print('$e && $p = $valid');
    return valid;
  });

  Function(String) get changeEmail => _email.sink.add;
  Function(String) get changePassword => _password.sink.add;

  submit() {
    final validEmail = _email.value;
    final validPassword = _email.value;
    print('final values: $validEmail && $validPassword');
    changeEmail('');
    changePassword('');
  }

  dispose() {
    _email.close();
    _password.close();
  }
}

When I press the submit button that calls this submit() function, I get the error messages for both of the text fields, because the values of email and password have changed behind the scenes, but they are not visually updated in the TextFields. Here are my StreamBuilders for my TextFields and Submit button:

Widget emailField(Bloc bloc) {
    return StreamBuilder(
      stream: bloc.email,
      builder: (context, snapshot) { // re-runs build function every time the stream emits a new value
        return TextField(
          onChanged: bloc.changeEmail,
          autocorrect: false,
          keyboardType: TextInputType.emailAddress,
          decoration: InputDecoration(
            icon: Icon(Icons.email),
            hintText: 'email address ([email protected])',
            labelText: 'Email',
            errorText: snapshot.error
          )
        );
      }
    );
  }

  Widget passwordField(Bloc bloc) {
    return StreamBuilder(
      stream: bloc.password,
      builder: (context, AsyncSnapshot<String> snapshot) {
        return TextField(
          onChanged: bloc.changePassword,
          autocorrect: false,
          obscureText: true,
          decoration: InputDecoration(
            icon: Icon(Icons.security),
            hintText: 'must be greater than 6 characters',
            labelText: 'Password',
            errorText: snapshot.error
          )
        );
      }
    );
  }

  Widget submitButton(Bloc bloc) {
    return StreamBuilder(
      stream: bloc.submitValid,
      builder: (context, snapshot) {
        return RaisedButton(
          child: Text('Logins'),
          color: Colors.blue,
          onPressed: !snapshot.hasData || snapshot.hasError || snapshot.data == false
            ? null
            : bloc.submit
        );
      }
    );
  }'

And here is the code I'm using for my validators in my Bloc:

class Validators {
  final validateEmail = StreamTransformer<String, String>.fromHandlers(
    handleData: (email, sink) {
      RegExp exp = new RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+");
      var valid = exp.hasMatch(email);
      if (valid) {
        sink.add(email);
      } else {
        sink.add('');
        sink.addError('Invalid email address!');
      }
    }
  );

  final validatePassword = StreamTransformer<String, String>.fromHandlers(
    handleData: (password, sink) {
      var valid = password.length >= 6;
      if (valid) {
        sink.add(password);
      } else {
        sink.add('');
        sink.addError('Password must be at least 6 characters long!');
      }
    }
  );
}

In my validators, I emit an empty string whenever there is an error. This makes it so the submitValid getter works when the user invalidates something that used to be valid.


Solution

  • I know it's been a long time, but that's my way for solving it.

    First, I've created a TextEditingController for my TextField. Then I've created two methods on my BLoC: updateTextOnChanged and updateTextElsewhere. On the fisrt one I just retrieved the value (because I need it to use later). On the second one I added a sink to update the controller on TextField.

    Widget:

      return StreamBuilder<String>(
          stream: bloc.streamText,
          builder: (context, snapshot) {
            _controller.text = snapshot.data;
            return Expanded(
                child: TextField(
                controller: _controller,
                onChanged: (value) => {bloc.updateTextOnChanged(value)},
              ),
            );
          }
       );
    

    Bloc:

      Stream<String> get streamText => _controllerTxt.stream;
      String _myText;
    
      void updateTextElsewhere(String value) {
        _controllerTxt.sink.add(value);
      }
    
      void updateTextOnChanged(String value) {
        _myText = value;
      }
    

    Then you just need to call updateTextElsewhere() whenever you need to update it outside onChanged.

    In you're case just add an empty string like: updateTextElsewhere("");