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.
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,
),
);
}
}