Search code examples
flutterdartrxdart

GlobalKey<FormState>().currentState.save() is falling when I submit a form in Flutter


Using bloc from rxdart: ^0.24.1

I am trying to save object on mysql. The first try the object get saved succefully, the second try, with a new object, it falling on formKey.currentState.save(). I am using GlobalKey<FormState>() in order to validate the form with Stream

My code is

class DetailGamePage extends StatefulWidget {
  @override
  _DetailGameState createState() => _DetailGameState();
}

class _DetailGameState extends State<DetailGamePage> {
  final formKey = GlobalKey<FormState>();
  GameBloc gameBloc;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (gameBloc == null) {
      gameBloc = Provider.gameBloc(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    Game _game = ModalRoute.of(context).settings.arguments;

    if (_game == null) {
      _game = Game(
          color: "#000000",
          description: "",
          env: "",
          isBuyIt: false,
          isOnBacklog: false);
    }

    return Scaffold(
        appBar: AppBar(
          iconTheme: IconThemeData(color: Colors.black),
          backgroundColor: Colors.white,
          title: Text(
            "Add Game",
            style: TextStyle(color: Colors.black),
          ),
          actions: [
            FlatButton(
                onPressed: () {
                  if (formKey.currentState.validate()) {
                    formKey.currentState.save();

                    Fluttertoast.showToast(msg: "Game saved");
                    setState(() {
                      gameBloc.saveOrUpdate(_game, gameBloc.name,
                          gameBloc.description, "listGame");
                    });
                    Navigator.pushReplacementNamed(context, "home");
                  }
                },
                child: Text(
                  (StringUtils.isNullOrEmpty(_game.id)) ? "Add" : "Update",
                  style: TextStyle(color: HexColor(_game.color), fontSize: 20),
                ))
          ],
        ),
        body: Form(
          key: formKey,
          child: Stack(children: <Widget>[
            _createBackground(context, _game),
            _createFormGame(context, _game, gameBloc)
          ]),
        ));
  }

  Widget _createBackground(BuildContext context, Game game) {
    final size = MediaQuery.of(context).size;

    final gradientTop = Container(
      height: size.height, //* 0.4,
      width: double.infinity,
      decoration: BoxDecoration(
          gradient: LinearGradient(
              colors: <Color>[HexColor(game.color), Colors.white])),
    );

    final circule = Container(
      width: 100.0,
      height: 100.0,
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(100.0),
          color: Color.fromRGBO(255, 255, 255, 0.1)),
    );

    return Stack(
      children: <Widget>[
        gradientTop,
        Positioned(
          child: circule,
          top: 90,
          left: 50,
        ),
        Positioned(
          child: circule,
          top: -40,
          right: -30,
        ),
        Container(
          padding: EdgeInsets.only(top: 80),
          child: Column(
            children: <Widget>[
              SizedBox(
                height: 10.0,
                width: double.infinity,
              ),
            ],
          ),
        )
      ],
    );
  }

  Widget _createFormGame(BuildContext context, Game game, GameBloc gameBloc) {
    final size = MediaQuery.of(context).size;
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          SafeArea(
              child: Container(
            height: 80.0,
          )),
          Container(
              width: size.width * 0.85,
              padding: EdgeInsets.symmetric(vertical: 50.0),
              margin: EdgeInsets.symmetric(vertical: 30.0),
              decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(5.0),
                  boxShadow: <BoxShadow>[
                    BoxShadow(
                        color: Colors.black26,
                        blurRadius: 3.0,
                        offset: Offset(0.0, 5.0),
                        spreadRadius: 3.0)
                  ]),
              child: Column(
                children: <Widget>[
                  Text("Foto", style: TextStyle(fontSize: 20.0)),
                  SizedBox(
                    height: 50.0,
                  ),
                  _createNameImput(gameBloc, game),
                  _createDescriptionImput(gameBloc, game),
                  Divider(
                    height: 30,
                    color: HexColor(game.color),
                    indent: 30,
                    endIndent: 20,
                  ),
                  _createWasGameImput(gameBloc, game),
                  Divider(
                    height: 30,
                    color: HexColor(game.color),
                    indent: 30,
                    endIndent: 20,
                  ),
                  _createToTheBacklogImput(gameBloc, game),
                  SizedBox(height: 60),
                  _createDeleteButton(gameBloc, game),
                  SizedBox(height: 60),
                ],
              ))
        ],
      ),
    );
  }

  @override
  void dispose() {
    gameBloc?.dispose();
    super.dispose();
  }

  Widget _createWasGameImput(GameBloc gameBloc, Game game) {
    return StreamBuilder(
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        return Container(
            padding: EdgeInsets.symmetric(horizontal: 20.0),
            child: SwitchListTile(
              activeColor: HexColor(game.color),
              title: Text("Do you have it?"),
              value: game.isBuyIt,
              onChanged: (bool value) {
                setState(() {
                  game.isBuyIt = value;
                });
              },
              secondary: IconButton(
                icon: Icon(Icons.shopping_cart),
                onPressed: null,
                color: HexColor(game.color),
              ),
            ));
      },
    );
  }

  Widget _createToTheBacklogImput(GameBloc gameBloc, Game game) {
    return StreamBuilder(
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        return Container(
            padding: EdgeInsets.symmetric(horizontal: 20.0),
            child: SwitchListTile(
              activeColor: HexColor(game.color),
              title: Text("To the backlog?"),
              value: game.isOnBacklog,
              onChanged: (bool value) {
                setState(() {
                  game.isOnBacklog = true;
                });
              },
              secondary: IconButton(
                icon: Icon(Icons.list),
                onPressed: null,
                color: HexColor(game.color),
              ),
            ));
      },
    );
  }

  Widget _createNameImput(GameBloc gamebloc, Game game) {
    return Column(children: [
      Container(
        padding: EdgeInsets.symmetric(horizontal: 20.0),
        child: TextFormField(
          textCapitalization: TextCapitalization.sentences,
          initialValue: game.name,
          onSaved: (value) {
            gameBloc.setName(value);
          },
          keyboardType: TextInputType.text,
          decoration: InputDecoration(
              labelText: "Name",
              icon: Icon(
                Icons.games,
                color: HexColor(game.color),
              )),
        ),
      ),
      Divider(
        height: 30,
        color: HexColor(game.color),
        indent: 30,
        endIndent: 20,
      ),
    ]);
  }

  Widget _createDescriptionImput(GameBloc gameBloc, Game game) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 20.0),
      child: TextFormField(
        textCapitalization: TextCapitalization.sentences,
        initialValue: game.description,
        onSaved: (value) {
          gameBloc.setDescription(value);
        },
        keyboardType: TextInputType.text,
        decoration: InputDecoration(
            labelText: "Description",
            icon: Icon(
              Icons.description,
              color: HexColor(game.color),
            )),
      ),
    );
  }

  Widget _createDeleteButton(GameBloc gameBloc, Game game) {
    if (StringUtils.isNotNullOrEmpty(game.id)) {
      return FlatButton(
          onPressed: () {
            showDialog(
                context: context,
                builder: (context) {
                  return AlertDialog(
                    content: Text("Do you wan to remove the game"),
                    actions: <Widget>[
                      FlatButton(
                          onPressed: () {
                            setState(() {
                              gameBloc.remove(game, "listGame");
                            });
                            Navigator.pop(context);
                            Navigator.pop(context);
                          },
                          child: Text("Yes")),
                      FlatButton(
                          onPressed: () => Navigator.of(context).pop(),
                          child: Text("No"))
                    ],
                  );
                });
          },
          child: Text("Remove Game"));
    } else {
      return Container();
    }
  }
}

This is the bloc

class GameBloc extends Validators {
  //Controller
  final _allDataGames = BehaviorSubject<List<Game>>();
  final _descriptionController = BehaviorSubject<String>();
  final _nameController = BehaviorSubject<String>();
  final _allMyListGamesByNameController = BehaviorSubject<List<Game>>();

  //Services
  GameService gameService = GameService();

  //get Data from streams
  Stream<List<Game>> get allGameData => _allDataGames.stream;
  Stream<List<Game>> get allGameByNameList =>
      _allMyListGamesByNameController.stream;
  Stream<String> get getDescriptionStream =>
      _descriptionController.stream.transform(validateDescription);

  Stream<String> get getNameStream =>
      _nameController.stream.transform(validName);

  //Observable
  Stream<bool> get validateDescriptionStream =>
      Rx.combineLatest([getDescriptionStream], (description) => true);

  Stream<bool> get validateNameStream =>
      Rx.combineLatest([getNameStream], (name) => true);

  //Set Stream
  Function(String) get setDescription => _descriptionController.sink.add;

  Function(String) get setName => _nameController.sink.add;

  //Get Stream

  //From repo
  void allGames() async {
    List<Game> games = await gameService.getAllDataGames();
    _allDataGames.sink.add(games);
  }

  //From my setting
  void allMyListGamesByName(String listName) async {
    List<Game> games = await gameService.allMyListGamesByName(listName);
    _allMyListGamesByNameController.sink.add(games);
  }

  void saveOrUpdate(
      Game game, String name, String description, String listGame) {
    game.name = name;
    game.description = description;
    if (StringUtils.isNullOrEmpty(game.id)) {
      game.id = Uuid().v1();
      gameService.add(game, listGame);
    } else {
      gameService.update(game);
    }
  }

  void remove(Game game, String listGame) {
    gameService.remove(game, listGame);
  }

  //Get Lastest stream value
  String get name => _nameController.value;
  String get description => _descriptionController.value;

  dispose() {
    _descriptionController?.close();
    _allMyListGamesByNameController?.close();
    _allDataGames?.close();
    _nameController?.close();
  }
}

The provider:

class Provider extends InheritedWidget {
  static Provider _imstance;
  final _gameBloc = GameBloc();

  factory Provider({Key key, Widget child}) {
    if (_imstance == null) {
      _imstance = new Provider._internal(key: key, child: child);
    }
    return _imstance;
  }

  Provider._internal({Key key, Widget child}) : super(key: key, child: child);

  static GameBloc gameBloc(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(Provider) as Provider)
        ._gameBloc;
  }

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return true;
  }
}

The error is:

════════ Exception caught by gesture ═══════════════════════════════════════════
Bad state: Cannot add new events after calling close

When I evaluate formKey.currentState.save(); I got:

formKey.currentState.save()
Unhandled exception:
Bad state: Cannot add new events after calling close
#0      _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:249:24)
#1      Subject._add (package:rxdart/src/subjects/subject.dart:141:17)
#2      Subject.add (package:rxdart/src/subjects/subject.dart:135:5)
#3      _StreamSinkWrapper.add (package:rxdart/src/subjects/subject.dart:167:13)

I was reading about this error, it mention the error is on Bloc singleston scope or dispose method.

What is happen?


Solution

  • When you navigate to home with Navigator.pushReplacementNamed(context, "home"), the _DetailGamePage<State> is being disposed, calling gameBloc?.dispose. This leaves _gameBloc instantiated with all streams closed.

    As you are using a Singleton Provider, when you navigate back to DetailGamePage, your save is trying to write to the closed streams.

    What you need to do is move the closure of the streams farther up the widget tree so as not to close them before you are done with them, perhaps at the app level OR re-instantiate _gameBloc if the streams are closed, loading the data from the repo again.