Search code examples
flutterbloc

Flutter BlocListener doesn't emit new states


I have the following Bloc:

class ClockBloc extends Bloc<ClockEvent, ClockState> {
      const ClockBloc() : super(const ClockState()) {
        on<ClockEventAddClock>(_addClock);
      }
      
      Future<void> _addClock(
        ClockEventAddClock event,
        Emitter<ClockState> emit,
      ) async {
        List<String> newItems = /* Some logic */;
        bool error = /* Some logic */;
        emit(ClockState(items: newItems, isError: error));
      }
    }

    class ClockState extends Equatable {
      const ClockState(this.items = const <String>[], this.isError = false,);
      final List<String> items;
      final bool isError;
      
        @override
      List<Object?> get props => [isError];
    }

Now, in my widget, I have a BlocListener which is supposed to display a dialog box (on button press) if state.isError = true. The thing is, the bloc may return the exact same state consecutively(when you keep pressing the button) - meaning the listener won't be triggered, and the dialog is never shown again.

How can I listen to the same state and trigger a dialog as long as state.isError = true?

UI:

  @override
  Widget build(BuildContext context) {
    return BlocListener<ClockBloc, ClockState>(
      listener: (context, state) {
        if (state.isError) {
          /*Display dialog*/
          // this will only show when isError goes from 'false' -> 'true',
          // and not from 'true' -> 'true''
        }
      }
    );
  }

I understand that since I'm using Equatable, the new state I'm emitting is compared against the previous one, and since they're identical, the new state is never emitted. So what approach would you choose? I thought about changing to some default state right before I emit the data back, but I'm sure there's a better solution.


Solution

  • If you look at most/all of the examples on bloclibrary.dev, you'll see that all state changes rely on an enum called status, and a copyWith method.

    Your state class in that format would look like this.

    enum ClockStatus { initial, loading, success, error } // or whatever makes sense for your case
    
    /// This is just for convenience so you can use `state.status.isError`
    extension ClockStatusX on ClockStatus {
      bool get isInitial => this == ClockStatus.initial;
      bool get isLoading => this == ClockStatus.loading;
      bool get isSuccess => this == ClockStatus.success;
      bool get isError => this == ClockStatus.error;
    }
    
    class ClockState extends Equatable {
      const ClockState({
        this.status = ClockStatus.initial,
        this.items = const <String>[],
      });
    
      final List<String> items;
      final ClockStatus status;
    
      ClockState copyWith({
        List<String>? items,
        ClockStatus? status,
      }) {
        return ClockState(
          items: items ?? this.items,
          status: status ?? this.status,
        );
      }
    
      @override
      List<Object?> get props => [status, items];
    }
    
    

    So then your updated bloc would look like this.

    class ClockBloc extends Bloc<ClockEvent, ClockState> {
      ClockBloc() : super(const ClockState()) {
        on<ClockEventAddClock>(_addClock);
      }
    
      Future<void> _addClock(
        ClockEventAddClock event,
        Emitter<ClockState> emit,
      ) async {
        emit(state.copyWith(status: ClockStatus.loading)); // this will always force a new state change, but in this case you don't have to react to it if you don't need to
    
        List<String> newItems = /* Some logic */;
        bool error = /* Some logic */;
    
        emit(
          state.copyWith(
            items: newItems,
            status: error ? ClockStatus.error : ClockStatus.success,
          ),
        );
      }
    }
    

    Then your listener callback would look like this

      @override
      Widget build(BuildContext context) {
        return BlocListener<ClockBloc, ClockState>(listener: (context, state) {
          if (state.status.isError) {
            /*Display dialog*/
            // since there is always an emit of ClockStatus.loading before ClockStatus.error, this will always show in an error state
          }
        });
      }
    

    That will sort out your issue.

    As a general suggestion, I recommend error handling with a try/catch approach

    Future<void> _addClock(
        ClockEventAddClock event,
        Emitter<ClockState> emit,
      ) async {
        List<String> newItems = [];
        emit(state.copyWith(status: ClockStatus.loading));
        try {
          // init your list
          emit(
            state.copyWith(
              items: newItems,
              status: ClockStatus.success,
            ),
          );
        } catch (e) {
          emit(
            state.copyWith(
              status: ClockStatus.error,
            ),
          );
        }
      }