Bloc: Best practice on showing loading indicator in bloc listener

Created on 12 Nov 2020  路  6Comments  路  Source: felangel/bloc

I couldn't find an example for that in the repo.
But I have several use cases, where I don't have a bloc builder, but only a bloc listener.
When I interact and e.g. press a button I trigger the LoadingState and want to show a loading dialog ON TOP of my current view.
I currently use the code above and always need to keep track of a isLoading variable in my view.
Are there better practices for this? (could only find this #1149 which doesn't provide code for the indicator)

body: BlocListener<MyBloc, MyState>(
  listener: (BuildContext context, MyState state) {
    if (state is MyStateLoading) {
      if (!loading) {
        LoadingDialog.show(context);
        loading = true;
      }
    } else {
      if (loading) {
        LoadingDialog.hide(context);
        loading = false;
      }
      //do some other listening actions
    }
  },
  child:...

this is my dialog. Note, that if I call hide when it is currently not shown, I pop my current view.

class LoadingDialog extends StatelessWidget {
  static void show(BuildContext context, {Key key}) => showDialog<void>(
        context: context,
        useRootNavigator: false,
        barrierDismissible: false,
        builder: (_) => LoadingDialog(key: key),
      ).then((_) => FocusScope.of(context).requestFocus(FocusNode()));

  static void hide(BuildContext context) => Navigator.pop(context);

  LoadingDialog({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async => false,
      child: Center(
        child: Card(
          child: Container(
            color: Colors.transparent,
            width: 80,
            height: 80,
            padding: EdgeInsets.all(12.0),
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}
question

Most helpful comment

@Babwenbiber Hi, I was facing the same problem before. Here is the solution I think is the best way to implement loading dialog.

  1. First, use Overlay instead of pushing dialog route. As far as I know BlocListener will keep listening after Navigator.of(context).push. So if you try to dismiss the dialog by using Navigator.of(context).pop while BlocListener start another LoadingDialog.show(context), that will dismiss the loading dialog wrongly. Moreover, if you are having complex navigation flow, managing the navigator is pain as hell as i had experienced.

  2. Second, using show dialog for presenting loading dialog will lead to problems which was mentioned. Because Navigator.of(context).push is not affecting the layout immediately, so if you are calling hide dialog too fast will cause to pop current page rather than hiding the dialog.

Enough talking here is the acceptable way I think:

class LoadingOverlay {
  OverlayEntry _overlay;

  LoadingOverlay();

  void show(BuildContext context) {
    if (_overlay == null) {
      _overlay = OverlayEntry(
        // replace with your own layout
        builder: (context) => ColoredBox(
          color: Color(0x80000000),
          child: Center(
            child: CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation(Colors.white),
            ),
          ),
        ),
      );
      Overlay.of(context).insert(_overlay);
    }
  }

  void hide() {
    if (_overlay != null) {
      _overlay.remove();
      _overlay = null;
    }
  }
}

class StatelessSample extends StatelessWidget {
  final LoadingOverlay _loadingOverlay = LoadingOverlay();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocListener<MyBloc, MyState>(
        listener: (context, state) {
          if (state is MyStateLoadInProgress) {
            _loadingOverlay.show(context);
          } else {
            _loadingOverlay.hide();
          }
        },
        child: Container(),
      ),
    );
  }
}

Hope it answers your question

All 6 comments

Hi! Why do you need a local variable to keep the state of something is loading? "loading" variable is redundant, when you use the bloclistener and listen to loadingState, you only have to show dialog or whatever you want and nothing else, the state of loading represent itself.

body: BlocListener<MyBloc, MyState>( listener: (BuildContext context, MyState state) { if (state is MyStateLoading) { LoadingDialog.show(context); } //do some other listening actions },

@alfredobs97
Lets say I have some states state1, state2, state3 and stateloading.
when I now have stateloading, then I can say LoadingDialog.show().
However, when I have state1,2,3 I have to close the dialog with LoadingDialog.hide().
If I don't close it, it will stay on top of my view.
So if I just say if state loading => show else hide, I have the problem explained in my post, which is that if the LoadingDialog is not active(switch from state 1 to 2), it will pop the current view.

Okay, I understand I think, in this case I would close the LoadingDialog in the other states, for example:

listener: (BuildContext context, MyState state) {
    if (state is MyStateLoading) {
        LoadingDialog.show(context);
    }
   else {
       LoadingDialog.close();
   }
      //do some other listening actions
  },`

But, I recommend you to refactor this decision to avoid if/else or if you see fine, okay!

Okay, I understand I think, in this case I would close the LoadingDialog in the other states, for example:

listener: (BuildContext context, MyState state) {
    if (state is MyStateLoading) {
        LoadingDialog.show(context);
    }
   else {
       LoadingDialog.close();
   }
      //do some other listening actions
  },`

But, I recommend you to refactor this decision to avoid if/else or if you see fine, okay!

I still have the problem, that I need to keep track of a local variable.
With your recommendation I would close my current view if I switch from state 1 to state 2 because the LoadingDialog does pop the view and I have no LoadingDialog open yet.
Regarding the refactoring: Thats why I asked for a best practice. I thaught it would be common to show some loading progress while loadingstate is set. How do others handle this? I dont want to use a builder for that.

@Babwenbiber Hi, I was facing the same problem before. Here is the solution I think is the best way to implement loading dialog.

  1. First, use Overlay instead of pushing dialog route. As far as I know BlocListener will keep listening after Navigator.of(context).push. So if you try to dismiss the dialog by using Navigator.of(context).pop while BlocListener start another LoadingDialog.show(context), that will dismiss the loading dialog wrongly. Moreover, if you are having complex navigation flow, managing the navigator is pain as hell as i had experienced.

  2. Second, using show dialog for presenting loading dialog will lead to problems which was mentioned. Because Navigator.of(context).push is not affecting the layout immediately, so if you are calling hide dialog too fast will cause to pop current page rather than hiding the dialog.

Enough talking here is the acceptable way I think:

class LoadingOverlay {
  OverlayEntry _overlay;

  LoadingOverlay();

  void show(BuildContext context) {
    if (_overlay == null) {
      _overlay = OverlayEntry(
        // replace with your own layout
        builder: (context) => ColoredBox(
          color: Color(0x80000000),
          child: Center(
            child: CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation(Colors.white),
            ),
          ),
        ),
      );
      Overlay.of(context).insert(_overlay);
    }
  }

  void hide() {
    if (_overlay != null) {
      _overlay.remove();
      _overlay = null;
    }
  }
}

class StatelessSample extends StatelessWidget {
  final LoadingOverlay _loadingOverlay = LoadingOverlay();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocListener<MyBloc, MyState>(
        listener: (context, state) {
          if (state is MyStateLoadInProgress) {
            _loadingOverlay.show(context);
          } else {
            _loadingOverlay.hide();
          }
        },
        child: Container(),
      ),
    );
  }
}

Hope it answers your question

Thanks a lot. The answer is a lot better than the navigation way.
The ideal way would have a static loading_overlay (so I don't need a local variable), but then you could not keep track of your state.
I am closing this issue, unless someone has a better way of doing it :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MahdiPishguy picture MahdiPishguy  路  3Comments

timtraversy picture timtraversy  路  3Comments

komapeb picture komapeb  路  3Comments

nhwilly picture nhwilly  路  3Comments

wheel1992 picture wheel1992  路  3Comments