Displaying a Snackbar in a Dialog with Flutter

Photo by Daniel Korpai / Unsplash

According to the material design specification, dialogs “inform users about a task and can contain critical information, require decisions, or involve multiple tasks.” Sometimes we’ll want these tasks to perform network requests, and it’s common practice to display errors in a Snackbar. I wanted to create this behavior in my recently released project for the 6x6 challenge (which you can read more about here) and found that it was slightly non-trivial. In this article I document my journey towards a solution that I’m happy with.

I’ll be demonstrating each step using a simplified example app. Our goal is to display a Snackbar after clicking a button in a dialog. You can follow along by downloading the GitHub repo. Each branch represents a step of the process

GitHub - MagsMagnoli/flutter-dialog-snackbar
Contribute to MagsMagnoli/flutter-dialog-snackbar development by creating an account on GitHub.

Let’s begin.

A Naive First Attempt

We know that a Snackbar must be shown using a context which contains a scaffold in its parent tree. One may naively attempt to trigger the Snackbar in the onPressed method of the button of the AlertDialog as demonstrated below:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
          child: Text('Show Dialog'),
          onPressed: () {
            showDialog(
              context: context,
              builder: (context) => AlertDialog(
                title: Text('SnackBar Dialog'),
                content: RaisedButton(
                  child: Text('Show SnackBar'),
                  onPressed: () {
                    Scaffold.of(context).showSnackBar(
                      SnackBar(content: Text('Hello, SnackBar!')),
                    );
                  },
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

Doing so will result in the following error:

We wrapped our showDialog call with a scaffold so what gives? By inspecting the widget hierarchy in DevTools we see our problem quite clearly:

AlertDialogs displayed using showDialog are placed above the MaterialApp and are not contained inside the calling MyHomePage which creates the Scaffold.

Iteration One: Scaffold

We can easily solve the scaffold issue from the previous section by wrapping our AlertDialog in another Scaffold. Be sure to use a Builder widget as the root of the body parameter to make the Scaffold available to the context given to the Scaffold.of which is used to display the Snackbar. We also need to make the background of the Scaffold transparent to persist the translucent effect of the AlertDialog.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
          child: Text('Show Dialog'),
          onPressed: () {
            showDialog(
              context: context,
              builder: (context) => Scaffold(
                backgroundColor: Colors.transparent,
                body: Builder(
                  builder: (context) => AlertDialog(
                    title: Text('SnackBar Dialog'),
                    content: RaisedButton(
                      child: Text('Show SnackBar'),
                      onPressed: () {
                        Scaffold.of(context).showSnackBar(
                          SnackBar(content: Text('Hello, SnackBar!')),
                        );
                      },
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

Pulling up DevTools again after these changes shows that our AlertDialog now has a Scaffold widget as its parent:

And running the app gives us the visuals we were expecting! 🎉

Sadly we are not done yet. 😓

If you interact with the app you’ll notice a problem. We’ve lost the ability to dismiss the dialog by touching outside of the content area. This is because we now have an invisible Scaffold filling the screen.

Iteration Two: Gestures

The dialog can be programmatically dismissed by calling the pop method on the Navigator class. We need to make this available to taps on the scrim area of the dialog and not on clicks onto the dialog itself. This can be achieved by using two GestureDetectors, one which wraps the entire Scaffold and makes the call to dismiss, and another that wraps the AlertDialog and prevents taps from being propagated to the parent one.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
          child: Text('Show Dialog'),
          onPressed: () {
            showDialog(
              context: context,
              builder: (context) => GestureDetector(
                onTap: () => Navigator.of(context).pop(),
                child: Scaffold(
                  backgroundColor: Colors.transparent,
                  body: Builder(
                    builder: (context) => GestureDetector(
                      onTap: () {},
                      child: AlertDialog(
                        title: Text('SnackBar Dialog'),
                        content: RaisedButton(
                          child: Text('Show SnackBar'),
                          onPressed: () {
                            Scaffold.of(context).showSnackBar(
                              SnackBar(content: Text('Hello, SnackBar!')),
                            );
                          },
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

This is the final widget hierarchy:

That’s it!

We now have a Snackbar enabled AlertDialog that we can use in our projects. Thanks for reading!