Bloc: [Feature Request] Allow Producing duplicate states

Created on 6 Oct 2020  路  13Comments  路  Source: felangel/bloc

It would be really helpful if we can produce duplicate states. It will help with the handing of one-time states eg. Navigation, Error messages, Pop-ups, etc..
This was also discussed at #362

How do we control duplicate states then?

  • With the help of listenWhen and buildWhen

Can we get around this by not extending Equatable?

  • Yes. This allows producing duplicate states but creates extra work whenever a comparison is needed.
    eg. equals comparison in testing

Reference to the code snippet
https://github.com/felangel/bloc/blob/93236d623d5d9833ca18bfc843ac832a0956987c/packages/bloc/lib/src/bloc.dart#L242

Thank you for this awesome library :+1:

question waiting for response

All 13 comments

Hi @karadkar 馃憢
Thanks for opening an issue!

You can produce duplicate states currently. By default if you do not extend Equatable or override == and hashCode each state instance is treated as unique 馃憤

class MyState {}

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyState());

  @override
  Stream<MyState> mapEventToState(MyEvent event) async* {
    yield MyState();
    await Future.delayed(const Duration(seconds: 3));
    yield MyState();
  }
}

The above code will trigger two state changes.

I'm guessing your states are overriding == and hashCode either via Equatable or Freezed.
Hope that helps 馃憤

Hi @felangel
Yes, I'm using Equatable in my states. It really helps with the equality checks in tests and other places.
Is there any way we can produce duplicate states while also having a standard == and hashCode implementation?
Or any on-demand configuration which will allow us to do this?

Hi @karadkar 馃憢

The quickest way would be to add a DateTime or some unique id field to your state class.

+1 for this feature request. I don't think it's a good idea to remove Equatable from states.

@karadkar and @SaeedMasoumi why do you feel this is needed? If the state of the bloc has not changed why do you want to notify listeners? Can you please provide a use-case? Thanks! 馃檹

@felangel , I suppose, that the case is defining equals() and hashcode() as unique constraight in DB, when developers try to dispatch entity with updated fields but with the same keySet, they have such problems.

@felangel One more example can be like this:
Assume that users can log in to the app as a guest or as a registered user. In different screens, we need to show specific widgets based on their login state. So, we create a reusable Bloc, called UserBloc, which dispatch the user state (including their tokens, profile and etc). In this case, same state can be dispatch multiple times.
Also:

  • We need equality for storing/retreiving data to/from database.
  • On large scale projects, personally I prefer to use super_enum to mimic sealed classes and avoid duplicate states like Error, Loadinh and etc. under the hood, super_enum implements equatable for the generated classes which make sense.

@vladimirshramov if the fields have been updated shouldn't the values no longer be equal?

@SaeedMasoumi but if the same state is emitted why would you expect the UI to update? The user in that case would not have changed.

@felangel Because of different screens, For example:

  • User is on the Home page and we show a popup to encourage him to register on our app (so we need to get the state of the user from our bloc)
  • Then the user clicks on My Order button and he will be navigated to a new screen, in this screen we show him a widget that tells him he can not see any order because he is not registered. Again we need to get the user state, but the same instance of the state can not be dispatch, So, no state will be received by the widget.

I think it's better to have a default implementation for buildWhen or listenWhen which ignores equal states.

@felangel You are right. And I do not add +1 to this request). What i mean is that equals defined not well but it happens. Imho it will be helpful to add one more demo for {one repo + one bloc with complex state + two consumers} case. Some people use your bloc sample for much more complex cases, when repo is needed and reading data using getter is not safe, because it may be not consistent during async operations chain.

class MessageState {
  final String message;
  final int someData;

  MessageState(this.someData, this.message);
}

BlocListener<Bloc, State>(
  listener: (context, state) {
    if (state is MessageState) {
      _showPopup(state);
    }
  },
);

@felangel
> If the state of the bloc has not changed why do you want to notify listeners? Can you please provide a use-case?

  • We need to show some "Error message" when the user does a wrong action. Users may do that wrong action again so we'll need to show that same message again.
  • Another user-case can be of navigation.
    On ScreenA, Event -> Some operation then State(Navigate to ScreenB)
    Here, if the user comes back ScreenA from ScreenB and we may need to repeat these things.
  • When class don't have an equality check support, the writing tests become harder
test("message state",(){
  final state = bloc.state;
  expect(state.someData, equals(expectedState.someData));
  expect(state.message, equals(expectedState.message));
  // TODO: add future members here...
});

  • With proper == and hashCode implementation, tests can be easy
test("message state",(){
  expect(bloc.state, equals(expectedState));
});

does copyWith help? If yes how?

class MessageState {
  final String message;
  final int someData;

  MessageState(this.someData, this.message);
}

BlocListener<Bloc, State>(
  listener: (context, state) {
    if (state is MessageState) {
      _showPopup(state);
    }
  },
);

@felangel
> If the state of the bloc has not changed why do you want to notify listeners? Can you please provide a use-case?

  • We need to show some "Error message" when the user does a wrong action. Users may do that wrong action again so we'll need to show that same message again.
  • Another user-case can be of navigation.
    On ScreenA, Event -> Some operation then State(Navigate to ScreenB)
    Here, if the user comes back ScreenA from ScreenB and we may need to repeat these things.
  • When class don't have an equality check support, the writing tests become harder
test("message state",(){
  final state = bloc.state;
  expect(state.someData, equals(expectedState.someData));
  expect(state.message, equals(expectedState.message));
  // TODO: add future members here...
});
  • With proper == and hashCode implementation, tests can be easy
test("message state",(){
  expect(bloc.state, equals(expectedState));
});

I had the same issue as the user could repeatedly trigger an event, and we would like to show the same error message to the user over and over again. (Such as click some button when the app doesn't have an internet connection), in the end, we added a DateTime property to the failure state as the failure did happen at a different time when the user triggered the action, looks like it works for us.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MahdiPishguy picture MahdiPishguy  路  3Comments

nhwilly picture nhwilly  路  3Comments

shawnchan2014 picture shawnchan2014  路  3Comments

nerder picture nerder  路  3Comments

ricktotec picture ricktotec  路  3Comments