flutter bloc - wrong state is sent from bloc to widget

972 views Asked by At

It seems like bug in bloc v0.11.2

I have the following Event/State:

class DeleteReceipt extends ReceiptEvent {
  final Receipt receipt;
  DeleteReceipt(this.receipt) : super([receipt]);
}

class ReceiptDeleted extends ReceiptState {
  final Receipt receipt;
  ReceiptDeleted(this.receipt) : super();
}

and the following code in bloc:

if (event is DeleteReceipt) {
  var delReceipt = event.receipt;
  await _receiptDao.delete(delReceipt);
  print("deleting: " + delReceipt.snapshot.documentID);
  yield ReceiptDeleted(delReceipt);
}

and my widget I have:

      if (state is ReceiptDeleted) {
        print("delete: "+state.receipt.snapshot.documentID);
        receipts.delete(state.receipt);
      }

and when I do: _receiptBloc.dispatch(DeleteReceipt(receipt)); the first time I get:

I/flutter (28196): deleting: AzgAzcn5wRNFVd7NyZqQ
I/flutter (28196): delete: AzgAzcn5wRNFVd7NyZqQ

which is correct, but the second time I do _receiptBloc.dispatch(DeleteReceipt(receipt)); on a different receipt, I get:

I/flutter (28196): deleting: d4oUjrGwHX1TvIDr9L2M
I/flutter (28196): delete: AzgAzcn5wRNFVd7NyZqQ

You can see that in the second time the DeleteReceipt event was received with the correct value, but the ReceiptDeleted State was received with the wrong value, and then it just get stuck like this, it never fires ReceiptDeleted State with the correct value, only with the first value.

My app is not trivial, and I have set many events and state in the past, and it worked with no issue (except this one, that probably is related flutter bloc state not received)

Basically I let the user create photos of receipt, that are persistent (using bloc/firestore), and I want to let the user delete them, so when the user click on a receipt, it opens in a new screen:

Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) {
      return ReceiptDetailPage(widget.receipt);
    },
  ),

and when the user click on delete, I show a dialog, and delete the receipt if is OK

var result = await showDialog(
  context: context,
  builder: (BuildContext dialogCtxt) {
    // return object of type Dialog
    return AlertDialog(
      title: new Text(AppLocalizations.of(context).deleteReceiptQuestion),
      actions: <Widget>[
        // usually buttons at the bottom of the dialog
        new FlatButton(
          child: new Text(AppLocalizations.of(context).cancel),
          onPressed: () {
            Navigator.of(dialogCtxt).pop("cancel");
          },
        ),
        new FlatButton(
          child: new Text(AppLocalizations.of(context).ok),
          onPressed: () {
            Navigator.of(dialogCtxt).pop("OK");
          },
        ),
      ],
    );
  },
);
if (result == 'OK') {
  Navigator.of(context).pop();
  _receiptBloc.dispatch(DeleteReceipt(receipt));
}
1

There are 1 answers

0
Elia Weiss On

Solution:

add state/event:

class EmptyState extends ReceiptState {}
class EmptyEvent extends ReceiptEvent {}

after receiving the delete state do:

      if (state is ReceiptDeleted) {
        print("delete: "+state.receipt.snapshot.documentID);
        receipts.delete(state.receipt);
        _receiptBloc.dispatch(EmptyEvent()); // add this line
      }

and add this to your bloc

if (event is EmptyEvent) {
  yield EmptyState();
}

This will cause an empty event and state to be fired and will clear the problem

Explain: I noticed that once I fire a State, the block provider will send that state every time I change a screen, which is strange since the app is receiving a Delete State many time. this is not a problem in my case, since the code will try to delete an element that is already delete and will fail quietly:

  void delete(Receipt receipt) {
    try {
      Receipt oldReceipt = receipts.firstWhere(
          (r) => r.snapshot.documentID == receipt.snapshot.documentID);
      receipts.remove(oldReceipt);
    } catch (e) {
      print(e);
    }
  }

NOTE: this seems to happen with all State that the app is firing, not only the Delete state

So I guest that if I will fire an empty event, it will clear the old Delete state, and will somehow fix the issue, and WALLA...

Note that I didn't had to actually listen to the EmptyState any where in my code

MORE INFO:

I realize that although the bloc seems to loose state, also my design is wrong, because the Data Structure should be updated in the bloc, once the event is received and not in the widget, when the state is received (or not received in this case, which cause the bug)

Initially I used bloc with sembast, but then I wanted the data to be sync with the remote DB, so I replaced sembast with firestore.

but that causes the load time to go from nothing, to more than 2 seconds, and that is a problem since in the original design I load all the data from the DB on every update.

So I tried to update the store and the UI seperatly, ie. instead of reading all the data, I keep a List in my widget and update the widget when the state changes - per update/delete state.

That was a problem, since many state were lost (especially when the user click fast - which cause many events/states to fire)

So I guess a correct solution would be to manage the in-memory Data in a separate Service, and update the Data when the Event is received, and then read all data from the Service instead of the store (when possible)