Bloc: is it possible to yield 2 time the same state?
Asked Answered
S

6

24

In the login view, if the user taps on the login button without having inserted his credentials, the LoginFailState is yield and the view reacts to it. If he taps again, this LoginFailstate is yield again, but the view doesn't react to it. So, is there a way to yield more times the same state?

There is some code to better explain my situation:

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  @override
  LoginState get initialState => LoginUninitialized();

  @override
  Stream<LoginState> mapEventToState(LoginEvent event) {
    if (event is loginButtonPressed) {
      yield LoginFailState();
    }
  }

View:

 @override
  Widget build(BuildContext context) {
    return BlocBuilder(
      bloc: _loginBloc,
      builder: (BuildContext context, LoginState state) {
    if (state is LoginFail) {
        print ('Login fail');
    }
    return Column(
          ...
    )
Shirring answered 2/7, 2019 at 10:43 Comment(0)
G
19

You can receive an update for the "same" State if you don't extend Equitable, or implement your own '==' logic which makes the two LoginFailStates equal.

The solution is to yield a different State in between, like in the Bloc example.

yield LoginLoading();

It gets called on every login button tap. Felangel's LoginBloc example.

Gretchen answered 18/11, 2019 at 14:19 Comment(0)
C
18

Problem

When you try to emit a new state that compares equal to the current state of a Bloc, the new state won't be emitted.

This behavior is by design and is discussed here.

When I say "compares equal" I mean that the == operator for the two state objects returns true.

Solution

There are two proper approaches:

  1. Your state class should NOT extend Equatable. Without Equatable, two objects of the same class with the same fields will NOT compare as equal, and this new state will always be emitted.
  2. Sometimes you need your state class to extend Equatable. In this case, just add the result of the identityHashCode(this) function call to your props getter implementation:
class NeverEqualState extends Equatable {

  @override
  List<Object?> get props => [identityHashCode(this)];
}

Note that I use identityHashCode that works regardless the operator == is overloaded or not. In contrast, hashCode will not work here.

Warning:

  1. Do not use random values in the getter implementation List<Object> get props => [Random().nextDouble()];. Random variables are random, meaning that with extremely low probability you still might get two equal values in a sequence that will break this workaround. This is extremely unlikely, so it's not possible to reproduce and debug this.
  2. You can and should include fields in your get props implementation, but keep in mind that when all fields compare as equal the objects will also compare as equal.
  3. Emitting some other state in-between two equal states works but it forces your BlocBuilder to rebuild part of UI and BlocListener to execute some logic. It's just inefficient.

Finally, why would you like to have a state class extend Equatable but still not compare equal? This might be needed when your state class is actually the root of a hierarchy, where some descendants need to implement the == operator properly, and some need to never compare equal. Here is the example:

class BaseMapState extends Equatable {
  const BaseMapState();

  @override
  List<Object?> get props => [];
}

class MapState extends BaseMapState {
  final Map<String, Report> reports;
  final Report? selectedReport;
  final LatLng? selectedPosition;
  final bool isLoadingNewReports;

  const MapState(
      {this.reports = const {},
      this.selectedReport,
      this.selectedPosition,
      this.isLoadingNewReports = false});

  @override
  List<Object?> get props => [
        ...reports.values,
        selectedReport,
        selectedPosition,
        isLoadingNewReports
      ];
}

class ErrorMapState extends BaseMapState {
  final String? error;

  const ErrorMapState(this.error);

  @override
  List<Object?> get props => [identityHashCode(this), error];
}

class NeedsAuthMapState extends ErrorMapState {
  const NeedsAuthMapState() : super('Authentication required');
}

class NoInternetMapState extends ErrorMapState {
  const NoInternetMapState() : super("No Internet connection");
}
Collapse answered 13/3, 2022 at 5:46 Comment(3)
Nice solution. Please don't use the const keyword while emitting the state. If you do so, the state will not be triggered the second time.Grainfield
This is accurate answer, but the problem is that you will not get the same state as you mentioned here Without Equatable, two objects of the same class with the same fields will NOT compare as equal, and this new state will always be emittedErgonomics
I may be wrong on this, but still, I believe I've had a problem with BlocBuilder not updating the state without proper Equatable implementationErgonomics
S
11

By default BLoC pattern will not emit state when the same state will be passed one after another. One way to do this is to pass your initial BLoC state after passing LoginFailState.

So after user clicks on the button with wrong credentials passed states will not be:

LoginFailState()
LoginFailState()

but

LoginFailState()
LoginEmptyState()
LoginFailState()
LoginEmptyState()

Which will make UI react to each of them.

But I think that the best and cleanest solution is to pass LoadingState from BLoC before passing LoginFailState().

You can follow the blog post that I have recently written regarding this topic.

Shul answered 20/11, 2019 at 14:5 Comment(3)
thank you, I got crazy with this issue! Didn't find any other solution to do it.Pettigrew
What if I emit the same state but with different arguments?Contractual
UPD: The state will not be emitted even with different arguments (Contractual
B
6

If you use Equitable and tries to emit two equal instances of the same State with different properties, make sure that you override props array. By overriding props array, Equitable will know how to compare state instances.

 class TablesLoadedState extends Equatable {
  final List<TableEntity> tablesList;

  TablesLoadedState(this.tablesList);

  @override
  List<Object> get props => [tablesList];
}

So, when bloc emits two instances of the same state with different values, these state-instances will be passed to BlocListener and UI will be updated according to new data.

Bebop answered 29/11, 2020 at 20:28 Comment(1)
Thanks, This should be the correct answerCalefaction
R
2

A late possible workaround would be adding a random double to the state get props, this way the state won't be equal and you can yield them one after the other if you want. also, Random().nextDouble() complexity is O(1) so you don't need to worry about performance

class LoginFailState extends LoginState {
  @override
  List<Object> get props => [Random().nextDouble()];
}
Rubi answered 18/1, 2022 at 23:59 Comment(0)
R
0

If you want to emit a new state every time the value changes, even if the changed value is the same as the previous one, you can modify the props getter in your state class to include an identifier, such as a timestamp or a counter.

final int timestamp;

 @override
  List<Object?> get props => [ timestamp];

final newTimestamp = DateTime.now().millisecondsSinceEpoch;
     emit(state.copyWith( timestamp: newTimestamp));

This will ensure that the state is always considered different, even if the value remains unchanged.

Note: It's not the best solution but it works.

And make sure to change this identifier when you want to change the state on the same value.

Rotenone answered 20/6, 2023 at 5:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.