Developed with 💚 by netglade
An extension to bloc state management library that manages unexpected exceptions in a code and displays them as customizable user-friendly error messages.
A library that provides a unified solution to code exception handling in blocs and cubits. In addition, it presents the errors to the user using a dedicated widget UnexpectErrorHandler
. This widget can be customized so that all the error dialogs and screens match your app design. Moreover, the exception processing is customizable, they can be either displayed, logged, or ignored. safe_bloc
also offers the advantage of an optional onUnexpectedError
callback, that is called each time an exception occurs. This can be suitable especially for exception logging.
This library also distinguishes between two types of error: error state and error actions:
-
Error states: These occur only during the initial screen loading. If the screen loading fails, there's no data to display to the user, and the
UnexpectErrorHandler
presents an error screen represented by theerrorScreen
parameter. -
Error actions: These typically happen when a user triggers an action (e.g., pressing a button) on an already loaded screen. In this scenario, we don't want to disrupt the user's experience by displaying an error screen and erasing the loaded data. Instead, the
safe_bloc
library simply shows an error dialog, informing the user that the action is currently unavailable. This ensures that the screen's existing data remains accessible to the user.
First, create an error state that will be emitted in case an exception occurs. This state must implement UnexpectedErrorBase
.
sealed class MyAppState {}
final class MyAppErrorState extends MyAppState implements UnexpectedErrorBase {
@override
final UnexpectedError error;
MyAppErrorState(this.error);
}
In case you are using Bloc
, extend your bloc with a SafeBloc
class and override its errorState
getter with the error state created in the previous step:
class MyAppBloc extends SafeBloc<MyAppEvent, MyAppState> {
// body
@override
MyAppState Function(UnexpectedError error) get errorState => MyAppErrorState.new;
}
Now, whenever you register a new event handler, use onSafe<EVENT>
event handler instead of standard on<EVENT>
:
onSafe<MyBlocEvent>(event, emit, {required trackingId}) async {
// do something
}
Similarly, if you are using Cubit
, extend your cubit with as SafeCubit
class and override the errorState
getter with the error state you have created in the first step. Then, wrap all the public cubit methods in a safeCall
method as follows:
class MyAppCubit extends SafeCubit<MyAppState> {
MyAppCubit(super.initialState);
FutureOr<void> someMethod() => safeCall((trackingId) {
// do something
});
@override
MyAppState Function(UnexpectedError error) get errorState => MyAppErrorState.new;
}
Alternatively, if your cubit method is synchronous, you can wrap it in safeCallSync
method.
Each time an exception occurs, it is caught by the parent class and MyAppErrorState
is emitted. This state contains an UnexpectedError
object with additional information about the exception including the exception itself.
Both onSafe
and safeCall
provide a unique trackingId
that can be used to track the user actions. Both methods also provide additional parameters:
devErrorMessage
(optional) - string message that is passed to theUnexpectedError
object, can be handy for loggingisAction
- bool that indicates if the method is an error action or error state. When set totrue
,UnexpectedErrorHandler
shows an error dialog or callsonErrorAction
callback if specified. When set tofalse
(default),UnexpectedErrorHandler
shows an error screen specified byerrorScreen
parameter.ignoreError
- bool that indicates whether the exception should be ignored. If set totrue
, the exception is caught, but MyAppErrorState is not emitted.onIgnoreError
(optional) - a callback that is invoked if the exception occurs andignoreError
parameter is set totrue
errorMapper
(optional) - a function that maps individual exceptions to bloc/cubit states. If null, all exceptions are mapped toMyAppErrorState
.
Additionally, SafeBloc
and SafeCubit
offer the option to override the onUnexpectedError
method. This method is invoked whenever an exception is thrown so that it can be useful for exception logging.
Use the UnexpectErrorHandler
in your widget tree in order to display the errors:
UnexpectedErrorHandler<MyAppBloc, MyAppState>(
errorScreen: (context, error) => Text(error.toString()),
onErrorAction: (context, error) {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(title: Text(error.toString()));
},
);
},
child: MyWidget(),
)
UnexpectErrorHandler
provides a parameter errorScreen
to display the errors during the initial screen loading and parameter onErrorAction
for the error actions. onErrorAction
callback is invoked if the isAction
parameter of the onSafe
/safeCall
method is set to true
.
This library makes use of the bloc_presentation library to handle the user error action events. bloc_presentation
adds another stream to the bloc/cubit in order to present one-time events (e.g. dialogs or snackbars) to the user. safe_bloc
library uses a BaseEffect
as a presentation event
and its inherited UnexpectedErrorEffect
class for exception handling. However, if you have specific presentation events you would like to use in your bloc or cubit, you can create your own implementation of presentation events and use them in combination with SafeBlocWithPresentation
or SafeCubitWithPresentation
like this:
class MyAppCubit extends SafeCubitWithPresentation<MyAppState, MyAppPresentationEvent> {
MyAppCubit(super.initialState);
FutureOr<void> someMethod() => safeCall((trackingId) {
// do something
});
@override
MyAppState Function(UnexpectedError error) get errorState => MyAppErrorState.new;
@override
MyAppPresentationEvent Function(UnexpectedError error) get errorEffect => MyAppErrorPresentationEvent.new;
}