Skip to content

Instantly share code, notes, and snippets.

@felangel
Created February 27, 2019 04:02
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save felangel/6614b9ce0a536ef462138a0ba698053a to your computer and use it in GitHub Desktop.
Save felangel/6614b9ce0a536ef462138a0ba698053a to your computer and use it in GitHub Desktop.
Bloc Exception Handling
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
abstract class AuthenticationEvent extends Equatable {
AuthenticationEvent([List props = const []]) : super(props);
}
class LoginEvent extends AuthenticationEvent {
final String loginRequest;
LoginEvent(this.loginRequest) : super([loginRequest]);
@override
String toString() {
return 'LoginEvent{loginRequest: $loginRequest}';
}
}
abstract class AuthenticationState extends Equatable {
AuthenticationState([List props = const []]) : super(props);
}
class AuthenticationStateUnInitialized extends AuthenticationState {
@override
String toString() => 'AuthenticationStateUnInitialized';
}
class AuthenticationStateLoading extends AuthenticationState {
@override
String toString() => 'AuthenticationStateLoading';
}
class AuthenticationStateSuccess extends AuthenticationState {
String user;
AuthenticationStateSuccess(this.user) : super([user]);
@override
String toString() => 'AuthenticationStateSuccess{ user: $user }';
}
class AuthenticationStateError extends AuthenticationState {
final String error;
AuthenticationStateError(this.error) : super([error]);
@override
String toString() => 'AuthenticationStateError { error: $error }';
}
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
AuthenticationBloc();
@override
AuthenticationState get initialState => AuthenticationStateUnInitialized();
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationState currentState,
AuthenticationEvent event,
) async* {
if (event is LoginEvent) {
yield AuthenticationStateLoading();
try {
final result = await _login(event.loginRequest);
yield AuthenticationStateSuccess(result);
} catch (e) {
yield AuthenticationStateError("error processing login request");
}
} else {
print("unknown");
}
}
Future<String> _login(dynamic request) async {
throw Exception();
}
@override
void onTransition(
Transition<AuthenticationEvent, AuthenticationState> transition,
) {
print(transition);
}
@override
void onError(Object error, StackTrace stacktrace) {
print('error in bloc $error $stacktrace');
}
}
void main() async {
AuthenticationBloc authBloc = AuthenticationBloc();
authBloc.state.listen((authState) {
print(authState);
});
authBloc.dispatch(LoginEvent('blah'));
await Future.delayed(Duration(seconds: 3));
authBloc.dispatch(LoginEvent('blah'));
}
@dalewking
Copy link

This is my big gripe about BLoC that it has no capability for handling errors (redux has the same issue). Other than in toy example programs, errors ARE NOT state. They are events to be reported to the UI and should not be persisted past that notification.

Take an example of a program that reads and displays a list of items from the network to the user. The user can pull to refresh to update the list of items. What if that refresh fails? We would like to pop up a dialog that the user can dismiss.

The fact that the refresh fails doesn't mean we should throw away the list we previously loaded. The model here would throw that list away unless we duplicate all of the loaded state inside the error state.

What if we get the error and the user dismisses it, navigates away from the app, then comes back? The state is still in this error state. Does he get a popup again?

The correct way to model errors is as a separate stream from state.

@felangel
Copy link
Author

felangel commented Sep 1, 2022

This is my big gripe about BLoC that it has no capability for handling errors (redux has the same issue). Other than in toy example programs, errors ARE NOT state. They are events to be reported to the UI and should not be persisted past that notification.

Take an example of a program that reads and displays a list of items from the network to the user. The user can pull to refresh to update the list of items. What if that refresh fails? We would like to pop up a dialog that the user can dismiss.

The fact that the refresh fails doesn't mean we should throw away the list we previously loaded. The model here would throw that list away unless we duplicate all of the loaded state inside the error state.

What if we get the error and the user dismisses it, navigates away from the app, then comes back? The state is still in this error state. Does he get a popup again?

The correct way to model errors is as a separate stream from state.

In the example you provided you can model your state as a single concrete class instead of multiple distinct subclasses:

enum MyStatus { initial, loading, failure, success }
class MyState<T> {
  const MyState({this.status = MyStatus.initial, this.data, this.error});

  final MyStatus status;
  final T? data;
  final Object? error;
  
  MyState copyWith(...) {...}
}

@dalewking
Copy link

I know you can include the error as field of state, but this is still problematic. If it is state (i.e. persists) then there is a chance that if you dismiss a notification, navigate away, then come back that you will get notified again because the error state is still there. I have seen it suggested in redux that you then have an event from the UI to tell the state management system to clear the error. But then you are adding extra complexity to make up for a flawed model of trying to make everything state. Errors like failure to communicate are not state, they are events and it is best to model them that way.

@felangel
Copy link
Author

If it is state (i.e. persists) then there is a chance that if you dismiss a notification, navigate away, then come back that you will get notified again because the error state is still there.

Can you share a minimal reproduction sample of the above scenario? Thanks!

@fnicastri
Copy link

@felangel @dalewking
I agree with @dalewking, when we have an error and the state is somehow persisted, something you have to do for example when you access Android camera (your activity can be killed and restarted after), there is much unneeded complexity in error management.
It is possible to manage it, no doubt, but would be much easyer, in my opinion, to have another, ephemeral, stream with error events streaming to the UI layer.
In this way we could have two classes of errors, one in the state for major errors that have sense to persist (a connection error? when we can trigger a reload maybe?) and error events to show just once, let say "informative errors".

emit(State());
emitUiEvent(UiEvent());

Anyway @felangel thanks for this awesome library!
I found it much more enjoyable and sane that other state management patterns in the flutter world ;)

@astubenbord
Copy link

@felangel
I agree with @fnicastri, I often find myself improperly implementing the bloc pattern because of this exact issue, which is unfortunate because I really like the way bloc handles state.

I found that the the easiest way to handle these "informative errors" in UI (by e.g. showing a snackbar with an error message) is to use a Cubit and try/catch the exception that is thrown from the call to myCubit.doSomething(). However, this approach ignores and therefore destroys the fully decoupled and reactive nature of the bloc pattern in my opinion. Also, when extending the Bloc class, we lose control over the data flow since emitting a new state is now fully decoupled from the call itself, so we can't try/catch errors thrown in the on<Event>(...) method.

Another solution would be to link to another "events" bloc passed to the bloc as an argument, to which these events are then added which can be listened to from a BlocListener<MyLinkedEventsBloc,...>. But this again adds a lot of complexity and even more boilerplate code to both each bloc and the widget using this bloc. It could also be argued that directly linking blocs like could is considered bad practice.

But these are just workarounds to an underlying problem and missing feature, at least in my opinion.

So, having a second stream on the bloc for UI events (as @fnicastri called it) and a new widget like a BlocUiEventListener (just following the terminology of @fnicastri here), or just add another property to the builder/consumer/listener widgets like void Function(UiEvent event) uiEventsListener to listen to non-persistent error events would make handling these errors a lot more convenient for us developers.

I'm sure that other issues might come along with this approach, but the inability to handle errors has always been my number one issue with this and similar state management solutions.

@fnicastri
Copy link

@astubenbord @felangel

I'm experimenting with this idea, I have a proof of concept implementation of this second stream in a Cubit, it works well but, again, is a very experimental and rough implementation.
Never had the time to do it in a proper way.

This is the implementation, again it's just a fast and dirty impl and tbh it's not something
I really worked on, just an experiment. Much of the stuff I just copy/pasted from the original classes to check if it was a good idea or not.
I'm sure it can be done in a much much better way.

import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

typedef BlocWidgetErrorListener<E> = void Function(
  BuildContext context,
  E error,
);

abstract class BlocBaseWithError<State, ErrorEvent> extends BlocBase<State>
    implements
        EmittableErrorEvent<ErrorEvent>,
        ErrorEventStreamable<ErrorEvent> {
  final StreamController<ErrorEvent?> _errorController =
      StreamController<ErrorEvent?>.broadcast();

  ErrorEvent? _errorEvent;

  BlocBaseWithError(super.initialState);

  @override
  ErrorEvent? get error => _errorEvent;

  @override
  Stream<ErrorEvent?> get errorStream => _errorController.stream;

  bool get isErrorClosed => _errorController.isClosed;

  @override
  Future<void> close() async {
    await _errorController.close();
    await super.close();
  }

  @override
  void emit(State state) {
    super.emit(state);
    // _errorController.sink.add(null);
  }

  @override
  void emitErrorEvent(ErrorEvent error) {
    try {
      if (isErrorClosed) {
        throw StateError('Cannot emit new errors after calling close');
      }
      _errorEvent = error;
      _errorController.add(error);
    } catch (error, stackTrace) {
      super.onError(error, stackTrace);
      rethrow;
    }
  }
}

abstract class CubitWithError<State, ErrorEvent>
    extends BlocBaseWithError<State, ErrorEvent>
    implements ErrorEventStreamableSource<ErrorEvent> {
  CubitWithError(State initialState) : super(initialState);

  @override
  Stream<ErrorEvent?> get errorStream {
    return super.errorStream;
  }
}

abstract class EEStreamable<E extends Object?> {
  /// The current [errorStream] of errors.
  Stream<E?> get errorStream;
}

/// An object that can emit new states.
abstract class EmittableErrorEvent<ErrorEvent extends Object?> {
  /// Emits a new [state].
  void emitErrorEvent(ErrorEvent state);
}

abstract class ErrorEventSink<ErrorEvent extends Object?> {
  /// Adds an [event] to the sink.
  ///
  /// Must not be called on a closed sink.
  void addErrorEvent(ErrorEvent event);
}

abstract class ErrorEventStreamable<ErrorEvent>
    implements EEStreamable<ErrorEvent> {
  /// The current [error].
  ErrorEvent? get error;
}

abstract class ErrorEventStreamableSource<ErrorEvent>
    implements ErrorEventStreamable<ErrorEvent>, Closable {}

class ErrorListener<B extends ErrorEventStreamable<E>, E>
    extends StatefulWidget {
  final BlocWidgetErrorListener<E?> errorListener;

  final E? error;
  final BlocListenerCondition<E?>? listenWhen;
  final B? bloc;
  final Widget? child;
  const ErrorListener({
    Key? key,
    required this.errorListener,
    this.error,
    this.listenWhen,
    this.child,
    this.bloc,
  }) : super(key: key);
  @override
  State<ErrorListener<B, E>> createState() => _ErrorListenerState<B, E>();
}

class _ErrorListenerState<B extends ErrorEventStreamable<E>, E>
    extends State<ErrorListener<B, E>> {
  late B _bloc;
  StreamSubscription<E?>? _subscription;
  late E? _previousError;
  @override
  Widget build(BuildContext context) {
    return widget.child ?? Container();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final bloc = context.read<B>();
    if (_bloc != bloc) {
      if (_subscription != null) {
        _unsubscribe();
        _bloc = bloc;
        _previousError = _bloc.error;
      }
      _subscribe();
    }
  }

  @override
  void didUpdateWidget(ErrorListener<B, E> oldWidget) {
    super.didUpdateWidget(oldWidget);
    final oldBloc = oldWidget.bloc ?? context.read<B>();
    final currentBloc = widget.bloc ?? oldBloc;
    if (oldBloc != currentBloc) {
      if (_subscription != null) {
        _unsubscribe();
        _bloc = currentBloc;
        _previousError = _bloc.error;
      }
      _subscribe();
    }
  }

  @override
  void dispose() {
    _unsubscribe();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    _bloc = context.read<B>();
    _subscribe();
  }

  void _subscribe() {
    _subscription = _bloc.errorStream.listen((errorEvent) {
      if (widget.listenWhen?.call(_previousError, errorEvent) ?? true) {
        widget.errorListener(context, errorEvent);
      }
      _previousError = errorEvent;
    });
  }

  void _unsubscribe() {
    _subscription?.cancel();
    _subscription = null;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment