How to do error handling with bloc pattern in flutter?
Asked Answered
O

3

32

Imagine I'm using a bloc to handle a network request. If the request fails, the way to handle the failure would be different depending on the platform. On my web app, I would like to redirect the user to an error page while on my IOS app I would like to show a dialog.

As bloc should only be used and shared to handle the business logic, and the error handling part has nothing to do with the business logic, we should ask the UI part to take care of the error handling.

The UI can send error callback to the bloc and the bloc will run it when an error happens. We can also handle the error in a platform-specific way by sending different callbacks in different platforms.

Then there come my two questions:

Is there a more appropriate way to do this?

How to send the callback to the bloc?

In flutter, we only have access to bloc after the initState life cycle method(for we get bloc from builder context, which only comes after initState). Then we can only send callback in the build method.

In this way, we will repetitively send callback to bloc every time rebuilding happens(these repetitions make no sense). With react, such one-time initialization could be done in life cycles such as componentDidMount. In flutter how do we reach the goal of running these initialization only once?

Orvie answered 5/10, 2018 at 1:14 Comment(3)
I think you're over-estimating the cost of passing a function reference.Iowa
I pretty new to Flutter and Dart but I was thinking what about a Stream of Errors so the bloc sink them and the UI could listen and acts?Gormand
Me also want to know this....Saw one here -> #52914993Caxton
R
3

This is how we handle it in my team:

First we build our main page (The navigation root) like this:

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SuspectEvent, SuspectState>(
        bloc: _bloc,
        builder: (context, state) {
          if (state.cameras.isEmpty) _bloc.dispatch(GetCamerasEvent());

          if (!_isExceptionHandled) {
            _shouldHandleException(
                hasException: state.hasException,
                handleException: state.handleException);
          }
        return Scaffold(
   ...

We declare the _shouldHandleException like this (still on the main page):

  _shouldHandleException(
      {@required bool hasException, @required Exception handleException}) {
    if (hasException) {
      if (handleException is AuthenticationException) {
        _isExceptionHandled = true;
        SchedulerBinding.instance.addPostFrameCallback((_) async {
          InfoDialog.showMessage(
                  context: context,
                  infoDialogType: DialogType.error,
                  text: 'Please, do your login again.',
                  title: 'Session expired')
              .then((val) {
            Navigator.popUntil(context, ModalRoute.withName('/'));
            this._showLogin();
          });
        });
      } else if (handleException is BusinessException) {
        _isExceptionHandled = true;
        SchedulerBinding.instance.addPostFrameCallback((_) async {
          InfoDialog.showMessage(
                  context: context,
                  infoDialogType: DialogType.alert,
                  text: handleException.toString(),
                  title: 'Verify your fields')
              .then((val) {
            _bloc.dispatch(CleanExceptionEvent());
            _isExceptionHandled = false;
          });
        });
      } else {
        _isExceptionHandled = true;
        SchedulerBinding.instance.addPostFrameCallback((_) async {
          InfoDialog.showMessage(
                  context: context,
                  infoDialogType: DialogType.error,
                  text: handleException.toString(),
                  title: 'Error on request')
              .then((val) {
            _bloc.dispatch(CleanExceptionEvent());
            _isExceptionHandled = false;
          });
        });
      }
    }
  }

On our block we have:


  @override
  Stream<SuspectState> mapEventToState(SuspectEvent event) async* {
    try {
      if (event is GetCamerasEvent) {

        ... //(our logic)
        yield (SuspectState.newValue(state: currentState)
          ..cameras = _cameras
          ..suspects = _suspects);
      }
      ... //(other events)
    } catch (error) {
      yield (SuspectState.newValue(state: currentState)
        ..hasException = true
        ..handleException = error);
    }
  }

In our error handling (on main page) the InfoDialog is just a showDialog (from Flutter) and it gets on top of any route. So the alert just needed to be called on the root route.

Regelation answered 15/7, 2019 at 18:24 Comment(0)
C
0

You can access the BLoC in the initState method if you wrap it in a scheduleMicrotask method, so that it runs after the initState method completed:

@override
void initState() {
  super.initState();
  // Do initialization here.
  scheduleMicrotask(() {
    // Do stuff that uses the BLoC here.
  });
}

You can also check out this answer to a different question outlining the Simple BLoC pattern, which just calls asynchronous methods directly on the BLoC instead of putting events into sinks.

That would allow code like this:

Future<void> login() {
  try {
    // Do the network stuff, like logging the user in or whatever.
    Bloc.of(context).login(userController.text, emailController.text);
  } on ServerNotReachableException {
    // Redirect the user, display a prompt or change this
    // widget's state to display an error. It's up to you.
  }
}
Cipolin answered 13/11, 2019 at 15:48 Comment(0)
T
0

You can use superEnum package to create states and events for a Bloc.(and here you will declare a state for the Error by doing this :

  @Data(fields: [DataField<Error>('error')])
  OrderLoadingFailedState,

(If anyone need an example of how to use it, please tell me i will show you an example)

Teeth answered 2/7, 2020 at 18:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.