Bloc: Error states Enum or generated by Freezed - bloc sending states issue

Created on 24 Sep 2020  ·  34Comments  ·  Source: felangel/bloc

We have simple bloc with 2 states Connected and ErrorState - and we do not need extra states (ConnectionStart or etc)

In case of unable to api.connect(); we sending ErrorState used to warn users by toast message.
(widget subscribed to the bloc and showing toast message + we have 'Connect again' button)

In case of no connection and user continually press 'Connect again' button - it will by just first alert displayed - all next 'Connect again' button presses will display no alerts

States example:

enum ConnectState { connected, errorState }

or

@freezed
abstract class ConnectState with _$ConnectState {
  const factory ConnectState.connected() = Connected;
  const factory ConnectState.errorState(String error) = ErrorState;
}

Bloc part example:

  @override
  Stream<ConnectState> mapEventToState(ConnectEvent event) =>
    event.when(
      connect: () async* {
       try {
          api.connect();
          yield Connected();
       } on ConnectionError catch {
          yield ErrorState('Unable to connect');
       }
    }

You could easily find issue in the bloc source code:
_bindEventsToStates method

return state == transition.nextState || _stateController.isClosed;

So as you could see issue appear because of '==' comparison and it is no way to override it to fix this issue.
Could I kindly ask you to fix this issue or add possibility to override it?

It is some other scenarios when this issue appear I just provide certain one I always have.
And override States '==' method (generate random hash or etc) or create unneeded states is not good solution.

Thanks in advance

duplicate question

Most helpful comment

Let's summarize:

  1. current bloc check doesn't work so I have to use district:
    bloc.whereState<Connected>(). distinct()
  2. in the block injected issue to advertise immutable classes, but lead younger developers to issues and bollerplate:
    bloc.whereState<ErrorState>().forEach(...);
  3. Bloc library created exclusively for Flutter UI and don't care other scenarios (server side or etc)

All 34 comments

Hi @zs-dima 👋

The issue you're facing is not really an issue and is certainly not bloc related. You should open an issue on freezed repo to allow for equality override on freezed classes, but I can tell you right now that Remi won't agree with that.

What you should do is:

yield Connecting();
api.connect();
yield Connected();

or have a DateTime field on your ErrorState to ensure subsequent yield of it will result in different states.

@RollyPeres
I do not need Connecting state for my case

More over states order is not guaranteed - so even this ugly boilerplate will not help to solve bloc issue

I gave you a realistic solution to your "issue".
I highly encourage you to read more about immutability and it's benefits as well as the bloc pattern to get a better overall understanding of what you're dealing with.

If you want to blame something for your equality not working as you expect that is freezed not bloc.
To demonstrate that, you can use simple classes instead of your freezed union and you'll notice that things are working as you expect.
If you want to be able to override an equality behavior that should be done on your freezed class not inside bloc implementation.

@RollyPeres you even had not read my comment about states order

More over states order is not guaranteed - so even this ugly boilerplate will not help to solve bloc issue

States order is guaranteed, they are emitted in the order of your yields.
With your case you'd always get: Connecting followed by Connected or Connecting followed by ErrorState.

@RollyPeres thanks you trying to help but

  1. I don't like crutches and ugly boilerplate

  2. Even this crutches and ugly boilerplate is not really solution

So I hope Felix will fix bloc issue finally

States order is guaranteed, they are emitted in the order of your yields.

States order is NOT guaranteed as transformEvents usually overridden

However you could make bloc transformEvents private to remove possibility to override it and you example will work :) but transformEvents is not private.

transformEvents has nothing to do with our discussion here, we're talking about processing of a single event and the states resulting from that processing.

User could press 'Connect again' button continually and you could not force him to do not do it
and transformEvents means states order is NOT guaranteed

any way - any beautiful solution are welcome
or bloc issue fix at least

I could modify your solution to make it mostly works, but it is really ugly:

try {
   api.connect();
   yield CrutchesToFixBlocIssueState();
   yield Connected();
} on ConnectionError catch {
   yield CrutchesToFixBlocIssueState();
   yield ErrorState('Unable to connect');
}

@zs-dima the following should work without:

abstract class ConnectState {}
class Connected extends ConnectState {}
class ErrorState extends ConnectState {
  ErrorState(this.error);
  final String error;
}

abstract class ConnectEvent {}
class ConnectStarted extends ConnectEvent {}

class ConnectBloc extends Bloc<ConnectEvent, ConnectState> {
  ConnectBloc() : super(null);

  @override
  Stream<ConnectState> mapEventToState(ConnectEvent event) async* {
    if (event is ConnectStarted) {
      try {
        await api.connect();
        yield Connected();
      } on ConnectionError {
        yield ErrorState('unable to connect');
      }
    }
  }
}

Hope that helps 👍

Closing for now but feel free to comment if you're still having trouble with the above implementation and I'm happy to help 😄

Hope that helps 👍

@felangel it is NOT help at all as I have to use freezed classes for complex states

and Enum for simple

ans you answer is not related to the question at all

Why do you have to use freezed and enums exclusively?

I know you want to make immutable classes popular, and I respect immutable classes usages in right places.

But bloc is not really good tool to advertise immutable classes.

However you free to make with your library whatever you want, even lead developers to exceptions as above and make them problems.

@zs-dima you didn't answer my question. As I mentioned before, if you prefer mutable state then there are other solutions out there 👍

@felangel
I want to use complex states with Freezed - to reduce bollerplate.

And for simple states I will prefer Enums to reduce bollerplate.

So any solution to the topic bug are welcome, but your code is not related to my question.

However your solution maybe could be nice for small demo apps.

@zs-dima you can't use enums because you want the same enum value to trigger a rebuild. Why would you why the UI to rebuild when the state has not changed?

I do not rebuilding UI, I am sending toast alerts with this bloc.
More over bloc could be used without UI at all.
It could be even pire dart without Flutter.
I mention it in the initial question.
So the best solution to be able to manage states to be one after another and not by overriding bloc comparison as well.

@zs-dima BlocListener is only triggered on state changes. If the enum value hasn't changed then by definition, no state change has occurred.

@felangel
I don't use flutter_bloc, I am subscribing on bloc stream directly to send toast alerts, I don't sure if BlocListener have to be changed as it could lead to unnedded UI rebuilds

@zs-dima state changes only occur when the state changes. In your case emitting ConnectState.connected twice should not trigger a state change because nothing has changed.

bloc.whereState<ErrorState>().forEach(...); for example

@felangel

@zs-dima state changes only occur when the state changes. In your case emitting ConnectState.connected twice should not trigger a state change because nothing has changed.

I could easily filter out or district ConnectState.connected state as I like in the same way

@felangel
More over it is one more point to remove check on bloc side - in case on filter
bloc.whereState<Connected>()
I could receive duplicated states even with current bloc - in case if any other states in between

I have already explained why I am against removing the check. Without that check, state will no longer need to be immutable which will cause more harm than good. We have given you several suggestions at this point for how to resolve your issue. If you insist of having mutable state then check out ChangeNotifier, StateNotifier, Mobx, etc...

Let's summarize:

  1. current bloc check doesn't work so I have to use district:
    bloc.whereState<Connected>(). distinct()
  2. in the block injected issue to advertise immutable classes, but lead younger developers to issues and bollerplate:
    bloc.whereState<ErrorState>().forEach(...);
  3. Bloc library created exclusively for Flutter UI and don't care other scenarios (server side or etc)

@zs-dima I strongly disagree with your statements and it seems you are misunderstanding the point of immutable state and the role it plays in bloc.

  1. You can use distinct but you need to make sure you're overriding == correctly
  2. Immutability is there to save you from issues not cause more issues. If immutability was not enforced, the library would no longer be predictable as states could change at any point in time from anywhere.
  3. That's incorrect (hence why we have separate packages like bloc, angular_bloc, flutter_bloc, etc...)

I think the problem you're facing is with how equality works in Dart and I would highly recommend reading more about it and looking at the freezed implementation for how equality is overridden.

@felangel looks you answer without reading my message.
current bloc state equally check even doesn't work, and lead to younger mistakes and bollerplate.
And you couldn't provide any beautiful solution to fix them.
Let's I will create new issue for bloc equally check, it is all I could help you.

You could be right if bloc restricted to the one state class only, but as bloc could send different states classes you totally missed what I talking about. Hope you will understand it one day.

@zs-dima I provided a solution and you disregarded it. @RollyPeres provided a different suggestion and you disregarded it as well.

The only thing bloc enforces is that states are different (since a state change requires a change). It's up to you as the developer to override that behavior as needed (you're in full control of operator== and hashCode).

A bloc should not emit duplicate states by default because it doesn't make sense -- a state change requires something to have changed where a change is defined as the equality comparison between the previous state and the newly emitted state.

Hope final solution you provided for the !!!Freezed and !!!Enum states will help youngers:
dart try {    api.​connect​();    ​yield​ ​CrutchesToFixBlocIssueState​();    ​yield​ ​Connected​(); } ​on​ ​ConnectionError​ ​catch​ {    ​yield​ ​CrutchesToFixBlocIssueState​();    ​yield​ ​ErrorState​(​'Unable to connect'​); } ​​

however I don't like it and will use different way, please let me know when you understand what I told this topic about, in other case you just wasted my time.

That is the solution you came up with, not the one we recommended.

If you want to use classes:

abstract class ConnectState {}
class Connected extends ConnectState {}
class ErrorState extends ConnectState {
  ErrorState(this.error);
  final String error;
}

abstract class ConnectEvent {}
class ConnectStarted extends ConnectEvent {}

class ConnectBloc extends Bloc<ConnectEvent, ConnectState> {
  ConnectBloc() : super(null);

  @override
  Stream<ConnectState> mapEventToState(ConnectEvent event) async* {
    if (event is ConnectStarted) {
      try {
        await api.connect();
        yield Connected();
      } on ConnectionError {
        yield ErrorState('unable to connect');
      }
    }
  }
}

If you want to use enums:

try {
   yield ConnectionState.connecting;
   await api.​connect​();
   yield​ ​ConnectionState.connected​;
} ​on​ ​ConnectionError​ ​catch​ {
  yield ConnectionState.error;
}

If you want to use freezed:

try {
   yield ConnectionInProgress();
   await api.​connect​();
   yield​ ConnectionSuccess();
} ​on​ ​ConnectionError​ ​catch​ {
  yield ConnectionFailure('unable to connect');
}

however I don't like it and will use different way, please let me know when you understand what I told this topic about, in other case you just wasted my time.

Closing this issue since I don't appreciate your tone. If you don't like any of the above solutions you can always fork the library and make whatever changes you'd like.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

abinvp picture abinvp  ·  3Comments

shawnchan2014 picture shawnchan2014  ·  3Comments

zjjt picture zjjt  ·  3Comments

krusek picture krusek  ·  3Comments

wheel1992 picture wheel1992  ·  3Comments