fluttermodal-dialog

Show dialog but without ModalBarrier


I have an application running in flutter WEB. I need few screens to open like a side menu.

I tried opening the screens as a dialog which works fine. But I need to open a dialog and still have all the interactions with the elements in the background screen while the dialog is open. The modal barrier prevents the interaction with the background. How do I do it now? Can anyone help me please?

Please don't suggest using stack! I have around 40 dialogs/modals to open so stack will not help me.


Solution

  • To achieve continued interaction with the background and avoid the ModalBarrier, you first need to construct your dialog outside of the regular build method to make it independent of the widget tree.

    You can accomplish this by using the Overlay widget, which allows you to overlay any widget on top of another widget within the app. Also define the Overlay widget at the app level to make it globally accessible and independent from the specific class or page being built. This will also enable you to navigate through different pages while the dialog is still open.

    Since the Overlay widget is independent, it requires to rebuild using markNeedsBuild to allow layout builder, so we call _overlayEntry?.markNeedsBuild(); to trigger a rebuild.

    You can find a simple implementation of this at my answer in: is there any way in flutter to persist dialog when moving from one screen to another? (I would also appreciate some credit on there)

    However in your situation, since you said you have 40+ dialogs, coding this was a bit more challenging. To manage multiple dialogs effectively, you need to use a list of overlays and have the ability to call the correct one upon closing. I managed to achieve this by using the list in conjunction with an identifier counter

    static List<_DialogEntry> _dialogEntries = [];
    static int _dialogIdCounter = 0;
    int dialogId = _dialogIdCounter++;
    

    The counter ensures that each dialog is uniquely identified so you can avoid certain errors regarding markNeedsBuild.

    You can then construct each dialog with an OverlayEntry:

    OverlayEntry(builder: (context) {
          return _DialogWidget(
            dialogId: dialogId,
            title: title,
            onDispose: () {
              _dialogEntries.removeWhere((entry) => entry.dialogId == dialogId);
            },
          );
        });
    
    _dialogEntries.add(_DialogEntry(
          dialogId: dialogId,
          overlayEntry: newOverlayEntry,
        ));
    

    _DialogEntry is the class model to mainly store the dialog's identifier dialogId and the _DialogWidget is the stateful widget we add for every list which also provides a method to remove/dispose the right dialog.

    Refer to this demo to understand how the above code works in a full working example (or expand the code snippet below):

    https://dartpad.dev/?id=67693f986403359a98081f130829ddcb

    Also note that in the demo, I've commented out the Positioned widget, which could be used to position each dialog at a different location. You will need to make further adjustments to it and play around to achieve your desired dialog positions, mine is just a basic example.

    import 'package:flutter/material.dart';
    import 'package:collection/collection.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class OverlayDialog {
      static List<_DialogEntry> _dialogEntries = [];
      static int _dialogIdCounter = 0;
    
      static void show(BuildContext context, String title) {
        int dialogId = _dialogIdCounter++;
        OverlayEntry newOverlayEntry = OverlayEntry(builder: (context) {
          // double position = dialogId * 50.0;
          return
              //Positioned(
    //         top: position,
    //         child:
              _DialogWidget(
            dialogId: dialogId,
            title: title,
            onDispose: () {
              _dialogEntries.removeWhere((entry) => entry.dialogId == dialogId);
            },
            // ),
          );
        });
    
        _dialogEntries.add(_DialogEntry(
          dialogId: dialogId,
          overlayEntry: newOverlayEntry,
        ));
        Overlay.of(context).insert(newOverlayEntry);
      }
    
      static void close(int dialogId) {
        final entry =
            _dialogEntries.firstWhereOrNull((entry) => entry.dialogId == dialogId);
        entry?.overlayEntry.remove();
      }
    }
    
    class _DialogEntry {
      final int dialogId;
      final OverlayEntry overlayEntry;
    
      _DialogEntry({required this.dialogId, required this.overlayEntry});
    }
    
    class _DialogWidget extends StatefulWidget {
      final int dialogId;
      final String title;
      final VoidCallback onDispose;
    
      _DialogWidget(
          {required this.dialogId, required this.title, required this.onDispose});
    
      @override
      _DialogWidgetState createState() => _DialogWidgetState();
    }
    
    class _DialogWidgetState extends State<_DialogWidget> {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Dialog(
            backgroundColor: Colors.grey,
            child: Container(
              constraints: BoxConstraints(
                minHeight: 80.0,
                maxHeight: MediaQuery.of(context).size.height,
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text(widget.title),
                  Text('content'),
                  ElevatedButton(
                    onPressed: () {
                      OverlayDialog.close(widget.dialogId);
                    },
                    child: const Text('Close Dialog'),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    
      @override
      void dispose() {
        widget.onDispose();
        super.dispose();
      }
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark(),
          home: ScreenA(),
        );
      }
    }
    
    class ScreenA extends StatelessWidget {
      void openDialogFunction(
          {required BuildContext context, required String title}) {
        OverlayDialog.show(context, title);
      }
    
      Widget kEButton(
          {required BuildContext context,
          required String title,
          bool showDialog = true}) {
        return Padding(
            padding: const EdgeInsets.all(3),
            child: ElevatedButton(
              onPressed: !showDialog
                  ? () {
                      print(title);
                    }
                  : () {
                      openDialogFunction(context: context, title: title);
                    },
              child: Text(title),
            ));
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Overlay Dialog Demo'),
          ),
          body: Center(
            child: Column(
              children: [
                kEButton(context: context, title: 'Dialog 1'),
                kEButton(context: context, title: 'Dialog 2'),
                kEButton(context: context, title: 'Dialog 3'),
                kEButton(context: context, title: 'Print test', showDialog: false),
              ],
            ),
          ),
        );
      }
    }