-
-
Save felangel/6614b9ce0a536ef462138a0ba698053a to your computer and use it in GitHub Desktop.
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')); | |
} |
@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 ;)
@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.
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;
}
}
Can you share a minimal reproduction sample of the above scenario? Thanks!