Search code examples
flutterdartgoogle-app-enginebloc

Rx.combineLatest2 not working as expected


I am a beginner in using Flutter. I am try to create some sample application in Flutter for create own application. I found a nice application in Flutter with bloc pattern.

It's just a login page. There is a email and password textbox in the form with validation. One validator for email and another validator for the password length. There is a submit button in the form, initially it's disabled, if the email and password successfully validated then the submit button enabled. it uses the bloc pattern architecture with rxdart package.

I have a issue in submit button validation, after enter the email and password field it's not enabled the button field.

Button validation code:

Stream<bool> get submitCheck => 
  Rx.combineLatest2(email, password, (e, p) => true);

main.dart

import 'package:bloc_login/pagetwo.dart';
import 'package:flutter/material.dart';
import 'package:bloc_login/bloc.dart';
import 'package:flutter/rendering.dart';

 void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
 @override
  Widget build(BuildContext context) {
return MaterialApp(
  home: MyHomePage(title: 'Flutter Demo Home Page'),
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.teal,
  ),
  debugShowCheckedModeBanner: false,
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
 @override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 changethePage(BuildContext context) {
Navigator.of(context)
    .push(MaterialPageRoute(builder: (context) => PageTwo()));
  }

 @override
   Widget build(BuildContext context) {
final bloc = Bloc();

return Scaffold(
  appBar: AppBar(
    title: Text("Bloc pattern"),
  ),
  body: SingleChildScrollView(
    child: Container(
      height: MediaQuery.of(context).size.height,
      padding: EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          StreamBuilder<String>(
            stream: bloc.email,
            builder: (context, snapshot) => TextField(
              onChanged: bloc.emailChanged,
              keyboardType: TextInputType.emailAddress,
              decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter Email",
                  labelText: "Email",
                  errorText: snapshot.error),
            ),
          ),
          SizedBox(
            height: 20,
          ),
          StreamBuilder<String>(
            stream: bloc.password,
            builder: (context, snapshot) => TextField(
              onChanged: bloc.passwordChanged,
              keyboardType: TextInputType.text,
              obscureText: true,
              decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter password",
                  labelText: "Password",
                  errorText: snapshot.error),
            ),
          ),
          SizedBox(
            height: 20,
          ),
          StreamBuilder<bool>(
              stream: bloc.submitCheck,
              builder: (context, snapshot) => RaisedButton(
                    color: Colors.tealAccent,
                    onPressed: (snapshot.data != null)
                        ? () => changethePage(context)
                        : null,
                    child: Text("Submit"),
                  ))
        ],
      ),
    ),
  ),
);
}
}

bloc.dart

import 'dart:async';
import 'package:bloc_login/validator.dart';
import 'package:rxdart/rxdart.dart';

class Bloc extends Object with Validators implements BaseBloc {
final _emailController = StreamController<String>();
final _passwordController = StreamController<String>();

Function(String) get emailChanged => _emailController.sink.add;
Function(String) get passwordChanged => _passwordController.sink.add;

Stream<String> get email => _emailController.stream.transform(emailValidator);
Stream<String> get password =>
  _passwordController.stream.transform(passwordValidator);

Stream<bool> get submitCheck =>
  Rx.combineLatest2(email, password, (e, p) => true);

@override
void dispose() {
_emailController.close();
_passwordController.close();
}
}

 abstract class BaseBloc {
   void dispose();
}

validator.dart

import 'dart:async';

mixin Validators {
 var emailValidator =
  StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
if (email.contains("@")) {
  sink.add(email);
} else {
  sink.addError("Email is not valid.");
}
});

var passwordValidator = StreamTransformer<String, String>.fromHandlers(
  handleData: (password, sink) {
if (password.length > 4) {
  sink.add(password);
} else {
  sink.addError("Password length should be greater than 4.");
}
});
}

What can I try next?


Solution

  • Combinelatest2 are fine, the problem is that you are creating a new bloc in each rebuild.

    So create the bloc in the initState method and dispose the bloc in the dispose method of the StatefulWidget.

    But also your StreamControllers have a Stream that supports only one single subscriber, so if you want that theStream of the StreamControllercan be listened to more than once, this need to be a broadcast stream, one of the ways to do it is using the StreamController .broadcast () constructor.

    P.D. If you are creating forms with the bloc pattern you can check flutter_form_bloc, it saves you a lot of code.


    Since dart 2.1 you don't need to extend an object to use a mixin

    class Bloc with Validators implements BaseBloc {
      final _emailController = StreamController<String>.broadcast();
      final _passwordController = StreamController<String>.broadcast();
      //...
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      Bloc _bloc;
    
      @override
      void initState() {
        super.initState();
        _bloc = Bloc();
      }
    
      @override
      void dispose() {
        _bloc.dispose();
        super.dispose();
      }
    
      changethePage(BuildContext context) {
        Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => PageTwo()));
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Bloc pattern"),
          ),
          body: SingleChildScrollView(
            child: Container(
              height: MediaQuery.of(context).size.height,
              padding: EdgeInsets.all(16),
              child: Column(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  StreamBuilder<String>(
                    stream: _bloc.email,
                    builder: (context, snapshot) => TextField(
                      onChanged: _bloc.emailChanged,
                      keyboardType: TextInputType.emailAddress,
                      decoration: InputDecoration(
                          border: OutlineInputBorder(),
                          hintText: "Enter Email",
                          labelText: "Email",
                          errorText: snapshot.error),
                    ),
                  ),
                  SizedBox(
                    height: 20,
                  ),
                  StreamBuilder<String>(
                    stream: _bloc.password,
                    builder: (context, snapshot) => TextField(
                      onChanged: _bloc.passwordChanged,
                      keyboardType: TextInputType.text,
                      obscureText: true,
                      decoration: InputDecoration(
                          border: OutlineInputBorder(),
                          hintText: "Enter password",
                          labelText: "Password",
                          errorText: snapshot.error),
                    ),
                  ),
                  SizedBox(
                    height: 20,
                  ),
                  StreamBuilder<bool>(
                      stream: _bloc.submitCheck,
                      builder: (context, snapshot) => RaisedButton(
                            color: Colors.tealAccent,
                            onPressed: (snapshot.data != null)
                                ? () => changethePage(context)
                                : null,
                            child: Text("Submit"),
                          ))
                ],
              ),
            ),
          ),
        );
      }
    }