Bloc: How to show/hide Dialog when state changed?

Created on 10 Mar 2019  路  35Comments  路  Source: felangel/bloc

Hello
I want to know how to show/hide Dialog when state changed.
For example in a bloc that have state isLoading i want to show a Dialog, and when the state isNotLoading i want to hide this.

But I read for hide dialog i need to use Navigator.pop(), so when I used that the Navigator pop the screen

I am trying with something like this:

bloc.state.listen(
  (state) {
    if(state.isLoading){
      showDialog(
        // ...
      )        
    }else{
      // Here I want to hide the dialog
      // This Pop the current page :(
     Navigator.of(context).pop() 
    }
  }
}

Also, I implement using a Container with Stack, but If I put in the body of Scafold, the Container with the loading widget not overlap the AppBar. so for overlap the AppBar i need to put this in a Column in the body of the Scaffold.

I don't like this form, so i want to find how to use showDialog or maybe other solution.

thanks,

question

Most helpful comment

We should NEVER "do things" in response to state changes in the builder method of BlocBuilder because that method can be called many times by the Flutter framework. The builder method should be a pure function that just returns a widget in response to the state of the bloc.

I changed my code to use BlocListener to show a dialog based on a certain state after I read the warning from the link. In addition, I've confirmed that I don't need to call addPostFrameCallback with a listener. Thanks for your quick support!

All 35 comments

Hey @basketball-ico check out this gist and let me know if that's what you were looking for?

Closing this for now. Let me know if you're still having trouble or have any follow up questions 馃憤

Hi @felangel thanks for help me :)
Not works.
I use like this, and the navigator pop the dialog and the page

bloc.state.listen(
  (state) {
    if(state.isLoading){
      _dialogBloc.dispatch(ShowDialog());
    }else{
     _dialogBloc.dispatch(HideDialog());
    }
  }
}

No problem 馃憤
Are you able to share the full code with me? Did you try the gist I linked to above?

Yes, I create a sample Here based on your gist.

in the TODO you can see where pop all.

Thanks

Thanks for sharing! I took a look and you don鈥檛 need the snippet where you do bloc.state.listen. That is what BlocBuilder is for. If you just remove the listen snippet it should all work.
Let me know if that helps! 馃憤

Sorry, i don't understand :(.

In the example I create the someBloc.state.listen... because I want to show the dialog when this state is loading and hide when not.
The state change from loading to notLoading in 5 seconds with Future.delayed... when you press load data button.

if I remove the listen, never show de dialog based on someBlocState I think haha.

note:
if you comment this line works, but i don't know hot to hide the dialog.

Thanks

BlocBuilder calls the builder method anytime the bloc state changes. In the example you sent whenever a HideDialog event is dispatched it will update BlocBuilder which will hide the Dialog but since you also are listening to the bloc state you are going to execute the pop twice. Does that make sense?

Yes, that have sense if I listen the same bloc two times, but are two different blocs.
I update de gist
Sorry, i can't understand :(.

No worries! I鈥檓 not sure I understand your question so I鈥檓 sorry.

I don鈥檛 understand why do you have two blocs that are both trying to show/hide the dialog? I also don鈥檛 recommend directly listening to bloc state unless you have no choice. I would always recommend using BlocBuilder when you need to update ui based on state changes.

Mmm, I have 1 bloc that have the state loading, and i want to show a dialog when have this state, and hide when not. :(

How is that different from the original gist I linked?

Just rename the states from DialogVisible to Loaded and DialogHidden to Loading and I think it does exactly what you want.

Yes, sorry it works, thanks :), I don't know what i was thinking :( haha
but...
I need to reset the state, because when use hot reload, it pop the view, and for othe use case I need to reset this state to initial state

I update the gist

I try this but in the BlocBuilder only arrive the InitialDialogState and not DialogHidden

if (event is ShowDialog) {
      yield DialogVisible();
      await Future.delayed(Duration(seconds: 2));
      yield DialogHidden();
      // reset state
      yield InitialDialogState();
    } 

Great! So everything is fine now?

No :(, I don't know why in the bloc builder only not arrive the second yield (yield DialogHidden();)

if (event is ShowDialog) {
      yield DialogVisible();
      await Future.delayed(Duration(seconds: 2));
      yield DialogHidden(); // this not arrive in BlocBuilder
      yield InitialDialogState();
    } 

It's because you are resetting the state immediately after by yielding InitialDialogState.

If you remove

yield InitialDialogState();

so that the code looks like:

yield DialogVisible();
await Future.delayed(Duration(seconds: 2));
yield DialogHidden();

then it will work.

I need something like this issue

So I need to create reset event, and in the UI dispatch this event after Pop the dialog ?

class Reset extends DialogEvent {}
...
   else if (event is Reset) {
      yield InitialDialogState();
    }
...
...
   Navigator.pop(context);
   _dialogBloc.dispatch(Reset());
...

it works, but I prefer something like this because is less code and not need to dispatch reset events in the UI, but not work :( , @felangel so you have other way to do something like this? o what is your recommendation?

...
   yield DialogHidden(); 
   yield InitialDialogState();
...

Thanks 馃

@felangel Hello, sorry i can't understand why if I yield sequentially the BlocBuilder and the Delegate not show this states transitions

if (event is ShowDialog) {
      yield DialogVisible();
      yield DialogHidden();
      yield InitialDialogState();
    } 

@basketball-ico that's not the problem. If you override onTransition in the bloc and print the transitions you'll see that all transitions are occurring. The problem is in the UI we show the dialog when the widget is ready (so that we avoid exceptions for dirty widget) so in this case the hide dialog state change is happening but the UI is not updating right away and by the time it does the state is already back to the InitialDialogState. Does that help?

Yes, I am understand, so the only way to reset is dispatch a event to reset in te UI after Dialog is hidden ?

Yes, given the current setup I think that's the easiest way. It would help me give you advice if you could share more about what you're trying to accomplish. Usually, if you're just going to show a dialog or navigate to another screen you don't need a bloc since there's no business logic.

Thanks, 馃
I have a bloc at the top level of my app that have a state loading, so I want to show the loadingDialog when this bloc have this state, and remove when not,

Other blocs interact with this bloc for this reason I try to show in based on this state.
Is like your loginBloc and your AuthenticationBloc, the loginBloc dispatch event to AuthenticationBloc and I want to show the dialog based of the state of AuthenticationBloc not LoginBloc.

Now it is working, but i don't like the idea to call reset event.

Thanks for the details! I don't think you should have to call reset.
Can you share a link to the github repo? It would be much easier for me to help if I can run the app locally. Thanks!

SchedulerBinding.instance.addPostFrameCallback resolved my problem.

The recommended approach for this moving forward is to use BlocListener. Check out the SnackBar Recipe for more details.

@felangel If I use BlocListener, can I just showDialog in a listener? or still requires to call showDialog withaddPostFrameCallback. Thank you in advance. One more question, this is not related to flutter bloc but some posts used WidgetsBinding.instance.addPostFrameCallback to show a dialog instead of SchedulerBinding.instance. Does it have any difference using one over the other one in this scenario?

We should NEVER "do things" in response to state changes in the builder method of BlocBuilder because that method can be called many times by the Flutter framework. The builder method should be a pure function that just returns a widget in response to the state of the bloc.

I changed my code to use BlocListener to show a dialog based on a certain state after I read the warning from the link. In addition, I've confirmed that I don't need to call addPostFrameCallback with a listener. Thanks for your quick support!

@njovy you beat me to it haha 馃槃

Hi everyone, great thread going on here. I'm having issues similar to the ones discussed.

I'm trying to show 1. snackbar and 2. showDialog in response to state changes with bloc Listener.
The problem I'm having is that the showDialog will occur, but it won't automatically close itself when the state changes. Here's my code:

    return BlocListener<AuthBloc, AuthState>(
      listener: (context, state) {
        if (state is AuthError) {
          Flushbar(
            title: 'hi',
            message: 'hi',
            duration: Duration(seconds: 3),
          )..show(context);
        }
        if (state is AuthLoading) {
          showDialog(
              context: context,
              builder: (_) {
                return Center(
                    child: Container(
                  width: 30,
                  height: 30,
                  color: Colors.red,
                ));
              });
        }
      },

I've also tried doing

SchedulerBinding.instance.addPostFrameCallback((_) {
  ... show dialog stuff
})

but it didn't do anything either to solve my problem

Thanks!

Sorry, turned out this was a problem of how the dialog works when it opens and closes. I needed to Navigator.pop(context). Everything worked fine when I added a condition to my BlocListener like so:

      condition: (prevState, currentState) {
        if (prevState is AuthLoading) {
          Navigator.pop(context);
        }
        return true;
      },

thank you

Sorry, turned out this was a problem of how the dialog works when it opens and closes. I needed to Navigator.pop(context). Everything worked fine when I added a condition to my BlocListener like so:

      condition: (prevState, currentState) {
        if (prevState is AuthLoading) {
          Navigator.pop(context);
        }
        return true;
      },

Thank you, 100% worked !

i have a similar issue here, but i have both a listener and builder, i want to pop the showdialog if there is issue and return a widget if the state is successful or not, here is an example code block..

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

import 'index.dart';

class SendResetCodeForm extends StatefulWidget {
  @override
  _SendResetCodeFormState createState() => _SendResetCodeFormState();
}

class _SendResetCodeFormState extends State<SendResetCodeForm> {
  SendResetCodeBloc _bloc;

  @override
  void initState() {
    super.initState();
    _bloc = BlocProvider.of<SendResetCodeBloc>(context);
  }

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<SendResetCodeBloc, SendResetCodeState>(
      listener: (BuildContext context, SendResetCodeState state) {
        if (state is LoadingSendResetCodeState) {
          showDialog(
            context: context,
            barrierDismissible: false,
            child: Spinner(
              title: "Sending Reset Code, Please Wait ...",
            ),
          );
        }

        if (state is NetworkErrorSendResetCodeState) {
          Routes.sailor.pop(); // pop the loading state
        }
      },
      builder: (BuildContext context, SendResetCodeState state) {

        if (state is NetworkErrorSendResetCodeState) {
          return NoInternet(previousPage: SendResetCode());
        }

        return Form(); // use this form to insert data
      },
    );
  }
}

The issue here is that, the nointernet widget never shows, but the showdialog is popped, if i comment out the line for the popping of the showdialog, the network error page doesnt still show up, please what is the issue?

Sorry, turned out this was a problem of how the dialog works when it opens and closes. I needed to Navigator.pop(context). Everything worked fine when I added a condition to my BlocListener like so:

      condition: (prevState, currentState) {
        if (prevState is AuthLoading) {
          Navigator.pop(context);
        }
        return true;
      },

But this works only once. When i try clicking the button again (that pops up the same dialog) then the dialog gets stuck there loading, and this condition block no longer gets triggered....why?

@milanobrenovic can you share a link to a sample app? Usually this means you鈥檙e not yielding the loading state anymore and/or your states are Equatable so they aren鈥檛 considered different.

Hello, I'm a bit late to the discussion - I've been a lurker here for a while, but handling modals have been something that's been very confusing to me with flutter as a whole, since I primarily relied on showDialogs for that in the past as well - but being a web developer myself, I started to think that modals are widgets too, which made it easier to wrap my head around.

Anyway, instead of using showDialog to show modals for preventing user taps and showing background process progress, I just wrapped the whole widget with a Stack and added the following code:

BlocProvider<MyBloc>(
   create: (_) => MyBloc(),
   child: Stack(
     children: [
       MyWidget(),
       BlocBuilder<MyBloc, MyState>(
         buildWhen: (previous, current) => previous.loading != current.loading || previous.loadingMessage != current.loadingMessage,
         builder: (context, state) {
          if (!state.loading) return Container();

          return Stack(
            children: [
              ModalBarrier(
                color: Colors.black.withAlpha(100),
              ),
              Center(
                child: AlertDialog(
                  title: Text(state.loadingMessage ?? "Loading..."),
                  elevation: 0,
                )
              )
            ]
          );
        }
     ]
   )
)

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold();
  }
}

You can customize the dialog and the modal barrier as much as you want, and even create a reusable widget to contain the modal code. Plus, I find it much more "safe" than having showDialogs and popping from the Navigator since there's no concrete reference to what you're popping out from. I believe there are also some libraries in pub.dev that does similar things.

Also, awesome library @felangel ! This library is so much more simpler to work with ~that~ than just using plain streams and RxDart for state management! Just wanted to give you a shout out! :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rsnider19 picture rsnider19  路  3Comments

timtraversy picture timtraversy  路  3Comments

frankrod picture frankrod  路  3Comments

nhwilly picture nhwilly  路  3Comments

MahdiPishguy picture MahdiPishguy  路  3Comments