Display SnackBar on top of AlertDialog widget
Asked Answered
M

5

21

I have an AlertDialog widget that will cause a SnackBar to display when you tap on its Text. The SnackBar currently displays behind the AlertDialog barrier, in the background. I want the Snackbar to display on top of the transparent AlertDialog barrier instead. Is the behavior that I'm seeking possible to achieve in Flutter? I have created a brand new Flutter app and included only the relevant code to illustrate the use-case below, as well as a screenshot.

Main.dart Gist

@override
Widget build(BuildContext context) {
  WidgetsBinding.instance!.addPostFrameCallback((_) async {
    showDialog(
      context: context,
      builder: (BuildContext dialogContext) => AlertDialog(
        content: GestureDetector(
          onTap: () {
            ScaffoldMessenger.of(dialogContext).showSnackBar(SnackBar(
              content: const Text('snack'),
              duration: const Duration(seconds: 1),
              action: SnackBarAction(
                label: 'ACTION',
                onPressed: () {},
              ),
            ));
          },
          child: Center(
            child: Text('Show SnackBar!'),
          ),
        ),
      ),
    );
  });
  // This method is rerun every time setState is called, for instance as done
  // by the _incrementCounter method above.
  //
  // The Flutter framework has been optimized to make rerunning build methods
  // fast, so that you can just rebuild anything that needs updating rather
  // than having to individually change instances of widgets.
  return Scaffold(
    appBar: AppBar(
      // Here we take the value from the MyHomePage object that was created by
      // the App.build method, and use it to set our appbar title.
      title: Text(widget.title),
    ),
    body: Center(
      // Center is a layout widget. It takes a single child and positions it
      // in the middle of the parent.
      child: Column(
        // Column is also a layout widget. It takes a list of children and
        // arranges them vertically. By default, it sizes itself to fit its
        // children horizontally, and tries to be as tall as its parent.
        //
        // Invoke "debug painting" (press "p" in the console, choose the
        // "Toggle Debug Paint" action from the Flutter Inspector in Android
        // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
        // to see the wireframe for each widget.
        //
        // Column has various properties to control how it sizes itself and
        // how it positions its children. Here we use mainAxisAlignment to
        // center the children vertically; the main axis here is the vertical
        // axis because Columns are vertical (the cross axis would be
        // horizontal).
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'You have pushed the button this many times:',
          ),
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: Icon(Icons.add),
    ), // This trailing comma makes auto-formatting nicer for build methods.
  );
}

enter image description here

Mcglothlin answered 19/7, 2021 at 18:31 Comment(0)
T
12

The issue here is that showDialog uses the root navigator provided by MaterialApp. So when you show your dialog it is pushed completely over your scaffold. To solve this you need the navigator that is used to be a child of the scaffold that's showing the snackbars. So the following code adds this navigator, sets useRootNavigator to false to use this navigator, and importantly uses a BuildContext under the newly created navigator:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Navigator(      //New navigator added here
        initialRoute: '/',
        onGenerateRoute: (setting) {
          return MaterialPageRoute(
            builder: (context) => Center(
              child: Builder(builder: (context) {
                WidgetsBinding.instance!
                  .addPostFrameCallback((_) async {
                    showDialog(
                      context: context,
                      useRootNavigator: false,//Dialog must not use root navigator
                      builder: (BuildContext dialogContext) =>
                      AlertDialog(
                        content: GestureDetector(
                          onTap: () {
                            ScaffoldMessenger.of(dialogContext)
                              .showSnackBar(SnackBar(
                                content: const Text('snack'),
                                duration: const Duration(seconds: 1),
                                action: SnackBarAction(
                                  label: 'ACTION',
                                  onPressed: () {},
                                ),
                              ));
                          },
                          child: Center(
                            child: Text('Show SnackBar!'),
                          ),
                        ),
                      ),
                    );
                  });
                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      'You have pushed the button this many times:',
                    ),
                    Text(
                      '$_counter',
                      style: Theme.of(context).textTheme.headline4,
                    ),
                  ]);
              }),
            ));
        }),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

Result: enter image description here


Note that this solution does constrain the dialog size a bit and the app bar and floating action button is above the content, which may be undesirable. This can be solved just by adding another scaffold below the newly created navigator and moving those appbar/FAB properties down as desired. Example with AppBar below the modal:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Navigator(
          initialRoute: '/',
          onGenerateRoute: (setting) {
            return MaterialPageRoute(
                builder: (context) => Scaffold(
                    appBar: AppBar(
                      title: Text(widget.title),
                    ),
                    body: Center(
                      child: Builder(builder: (context) {
                        WidgetsBinding.instance!
                            .addPostFrameCallback((_) async {
                          showDialog(
                            context: context,
                            useRootNavigator: false,
                            builder: (BuildContext dialogContext) =>
                                AlertDialog(
                              content: GestureDetector(
                                onTap: () {
                                  ScaffoldMessenger.of(dialogContext)
                                      .showSnackBar(SnackBar(
                                    content: const Text('snack'),
                                    duration: const Duration(seconds: 1),
                                    action: SnackBarAction(
                                      label: 'ACTION',
                                      onPressed: () {},
                                    ),
                                  ));
                                },
                                child: Center(
                                  child: Text('Show SnackBar!'),
                                ),
                              ),
                            ),
                          );
                        });
                        return Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text(
                                'You have pushed the button this many times:',
                              ),
                              Text(
                                '$_counter',
                                style: Theme.of(context).textTheme.headline4,
                              ),
                            ]);
                      }),
                    )));
          }),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

Result: enter image description here

Tercentenary answered 19/7, 2021 at 19:39 Comment(1)
This is fantastic. A great explanation. Thanks a ton Christopher! Greatly appreciated!Mcglothlin
L
39

Update

Thanks to Amy, I realized that tapping on the barrier did not dismiss the dialog. Also, the code was causing to show multiple SnackBars due to the use of nested Scaffolds.

Check out the following model that fixes all issues:

showDialog
 |
 |
ScaffoldMessenger => "Set a scope to show SnackBars only in the inner Scaffold"
   |
   --- Builder => "Add a Builder widget to access the Scaffold Messenger"
        |
        --- Scaffold => "The inner Scaffold that is needed to show SnackBars"
             |
             --- GestureDetector => "Dismiss the dialog when tapped outside"
                  |
                  --- GestureDetector => "Don't dismiss it when tapped inside"
                       |
                       --- AlertDialog => "Your dialog"

Here is the implementation:

showDialog(
  context: context,
  builder: (context) => ScaffoldMessenger(
    child: Builder(
      builder: (context) => Scaffold(
        backgroundColor: Colors.transparent,
        body: GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTap: () => Navigator.of(context).pop(),
          child: GestureDetector(
            onTap: () {},
            child: AlertDialog(
              content: GestureDetector(
                onTap: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: const Text('snack'),
                      duration: const Duration(seconds: 1),
                      action: SnackBarAction(
                        label: 'ACTION',
                        onPressed: () {},
                      ),
                    ),
                  );
                },
                child: Center(
                  child: Text('Show SnackBar!'),
                ),
              ),
            ),
          ),
        ),
      ),
    ),
  ),
);

enter image description here

Old answer

ScaffoldMessenger shows SnackBar in the nearest descendant Scaffold. If you add another Scaffold before AlertDialog, it will use it instead of the root one which is left behind the dialog.

showDialog(
  context: context,
  builder: (BuildContext dialogContext) => Scaffold(
    backgroundColor: Colors.transparent, // Make Scaffold's background transparent
    body: AlertDialog(
      content: GestureDetector(
        onTap: () {
          ScaffoldMessenger.of(dialogContext).showSnackBar(SnackBar(
            content: const Text('snack'),
            duration: const Duration(seconds: 1),
            action: SnackBarAction(
              label: 'ACTION',
              onPressed: () {},
            ),
          ));
        },
        child: Center(
          child: Text('Show SnackBar!'),
        ),
      ),
    ),
  ),
);
Labor answered 19/7, 2021 at 20:7 Comment(1)
Then you can't click off the dialog to dismiss itFrap
T
12

The issue here is that showDialog uses the root navigator provided by MaterialApp. So when you show your dialog it is pushed completely over your scaffold. To solve this you need the navigator that is used to be a child of the scaffold that's showing the snackbars. So the following code adds this navigator, sets useRootNavigator to false to use this navigator, and importantly uses a BuildContext under the newly created navigator:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Navigator(      //New navigator added here
        initialRoute: '/',
        onGenerateRoute: (setting) {
          return MaterialPageRoute(
            builder: (context) => Center(
              child: Builder(builder: (context) {
                WidgetsBinding.instance!
                  .addPostFrameCallback((_) async {
                    showDialog(
                      context: context,
                      useRootNavigator: false,//Dialog must not use root navigator
                      builder: (BuildContext dialogContext) =>
                      AlertDialog(
                        content: GestureDetector(
                          onTap: () {
                            ScaffoldMessenger.of(dialogContext)
                              .showSnackBar(SnackBar(
                                content: const Text('snack'),
                                duration: const Duration(seconds: 1),
                                action: SnackBarAction(
                                  label: 'ACTION',
                                  onPressed: () {},
                                ),
                              ));
                          },
                          child: Center(
                            child: Text('Show SnackBar!'),
                          ),
                        ),
                      ),
                    );
                  });
                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      'You have pushed the button this many times:',
                    ),
                    Text(
                      '$_counter',
                      style: Theme.of(context).textTheme.headline4,
                    ),
                  ]);
              }),
            ));
        }),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

Result: enter image description here


Note that this solution does constrain the dialog size a bit and the app bar and floating action button is above the content, which may be undesirable. This can be solved just by adding another scaffold below the newly created navigator and moving those appbar/FAB properties down as desired. Example with AppBar below the modal:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Navigator(
          initialRoute: '/',
          onGenerateRoute: (setting) {
            return MaterialPageRoute(
                builder: (context) => Scaffold(
                    appBar: AppBar(
                      title: Text(widget.title),
                    ),
                    body: Center(
                      child: Builder(builder: (context) {
                        WidgetsBinding.instance!
                            .addPostFrameCallback((_) async {
                          showDialog(
                            context: context,
                            useRootNavigator: false,
                            builder: (BuildContext dialogContext) =>
                                AlertDialog(
                              content: GestureDetector(
                                onTap: () {
                                  ScaffoldMessenger.of(dialogContext)
                                      .showSnackBar(SnackBar(
                                    content: const Text('snack'),
                                    duration: const Duration(seconds: 1),
                                    action: SnackBarAction(
                                      label: 'ACTION',
                                      onPressed: () {},
                                    ),
                                  ));
                                },
                                child: Center(
                                  child: Text('Show SnackBar!'),
                                ),
                              ),
                            ),
                          );
                        });
                        return Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text(
                                'You have pushed the button this many times:',
                              ),
                              Text(
                                '$_counter',
                                style: Theme.of(context).textTheme.headline4,
                              ),
                            ]);
                      }),
                    )));
          }),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

Result: enter image description here

Tercentenary answered 19/7, 2021 at 19:39 Comment(1)
This is fantastic. A great explanation. Thanks a ton Christopher! Greatly appreciated!Mcglothlin
D
4

Instead of the SnackBar Use another_flushbar, It will Appear Above AlertDialog.

Flushbar(
    backgroundColor: Colors.red,
    message: S.of(context).choose_date,
    duration: Duration(seconds: Constants.TOAST_DURATION),
  ).show(context);

Result:

Flushbar Demo

Delicatessen answered 2/12, 2021 at 7:45 Comment(0)
E
2

hope this is what you are looking for enter image description here

import 'package:flutter/material.dart';

class SnackOverDialog extends StatefulWidget {
  SnackOverDialog({Key? key}) : super(key: key);

  @override
  _SnackOverDialogState createState() => _SnackOverDialogState();
}

class _SnackOverDialogState extends State<SnackOverDialog> {
  final GlobalKey<ScaffoldState> _scaffoldkey = new GlobalKey<ScaffoldState>();
  @override
  Widget build(BuildContext context) {
    ///* show snack
    _snackbar(BuildContext context) {
      _scaffoldkey.currentState!.showSnackBar(SnackBar(
        content: const Text('snack'),
        duration: const Duration(seconds: 1),
        action: SnackBarAction(
          label: 'ACTION',
          onPressed: () {},
        ),
      ));
    }

    ///* dialog
    _dialog(BuildContext context) {
      WidgetsBinding.instance!.addPostFrameCallback((_) async {
        showDialog(
          context: context,
          builder: (BuildContext dialogContext) => AlertDialog(
            content: Scaffold(
              key: _scaffoldkey,
              body: GestureDetector(
                onTap: () {
                  _snackbar(dialogContext);
                },
                child: Center(
                  child: Text('Show SnackBar!'),
                ),
              ),
            ),
          ),
        );
      });
    }

    return Scaffold(
      appBar: AppBar(
        title: Text("SNackBarOVerDialog"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _dialog(context),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


Erewhile answered 19/7, 2021 at 19:19 Comment(2)
There is no more showSnackBar method available with currentState in the latest flutter version.Eiser
try with ScaffoldMessengerErewhile
A
1

I found another solution. Just wrap your dialog in a Scaffold with transparent background.

However you still need the gesture detectors to pop the scope.


class Example extends StatelessWidget {
  const Example({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: TextButton(
          child: Text('open dialog'),
          onPressed: () => showDialog(
              context: context, builder: (context) => DialogWidget())),
    );
  }
}

class DialogWidget extends StatelessWidget {
  const DialogWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => Navigator.pop(context),
      child: Scaffold(
        backgroundColor: Colors.transparent,
        body: GestureDetector(
          onTap: (){},
          child: AlertDialog(
            actions: [
              TextButton(
                  onPressed: () {
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                      content: Text('hallo world'),
                      action: SnackBarAction(
                        label: 'click me!',
                        onPressed: () {},
                      ),
                    ));
                  },
                  child: Text("Allert"))
            ],
          ),
        ),
      ),
    );
  }
}

``
Arabele answered 4/11, 2023 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.