2

I have been working on a project which requires me to change the on-screen widget based on the mobile device's orientation.

For example, in the case of portrait orientation, I need to show a widget (let's say portrait widget) and in case of landscape orientation, I need to show another widget (let's call it landscape widget).

I have used OrientationBuilder

Actual problem: On orientation change, all the Dialogs and OptionMenu or any other Popup kind of Widget is not closed. How do I close them on orientation change?

Steps to reproduce the problem:

  1. Run the app code given below [example app for reproducing the issue]
  2. Do a long press on app body to see a dialog or click on OptionMenu on top-left
  3. Change device orientation, you'll see that the widget body has changed according to the Orientation but the Popuped widget is still visible.

Note: Please note that I need a global solution for this issue. Providing a solution around only this code specifically will not be of any use for me. This is an Example code which I have provided for better understanding of the problem, I don't use this code at all.

example app code:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return OrientationBuilder(builder: (context, orientation) {
      bool isLandscape = orientation == Orientation.landscape;
      return isLandscape ? Landscape() : Portrait();
    });
  }
}

class Portrait extends StatefulWidget {
  @override
  _PortraitState createState() => _PortraitState();
}

class _PortraitState extends State<Portrait> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: buildTitle(),
        actions: <Widget>[_buildOptionMenu(context)],
      ),
      body: GestureDetector(
        onLongPress: () {
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: buildTitle(),
            ),
          );
        },
        child: Container(
          color: Colors.blue.withOpacity(0.4),
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Center(
                child: buildTitle(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildOptionMenu(BuildContext context) {
    return PopupMenuButton(itemBuilder: (context) {
      var list = <String>['Portrait-Item-1', 'Portrait-Item-2'];
      return list
          .map<PopupMenuEntry<String>>(
            (e) => PopupMenuItem<String>(
              child: Text(e),
            ),
          )
          .toList();
    });
  }

  Text buildTitle() => Text('Portrait');
}

class Landscape extends StatefulWidget {
  @override
  _LandscapeState createState() => _LandscapeState();
}

class _LandscapeState extends State<Landscape> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: buildTitle(),
        actions: <Widget>[_buildOptionMenu(context)],
      ),
      body: GestureDetector(
        onLongPress: () {
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: buildTitle(),
            ),
          );
        },
        child: Container(
          color: Colors.orange.withOpacity(0.3),
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Center(
                child: buildTitle(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Text buildTitle() => Text('Landscape');

  Widget _buildOptionMenu(BuildContext context) {
    return PopupMenuButton(itemBuilder: (context) {
      var list = <String>[
        'Landscape-Item-1',
        'Landscape-Item-2',
        'Landscape-Item-3',
      ];
      return list
          .map<PopupMenuEntry<String>>(
            (e) => PopupMenuItem<String>(
              child: Text(e),
            ),
          )
          .toList();
    });
  }
}

I couldn't find any feasible solution. There are some solutions which require to listen for orientation changes and push/pop widgets based on new orientation.

But It is a bit too much to work with and requires to add the same type of code in both portrait and landscape widget. Which is also not scalable in case I need to handle addition orientations like, reverse-portrait and reverse-landscape.

Update

One of the solution which is feasible to do is pop all the widgets until the root widget and then push new widget based on orientation. This works but comes with a side effect.

For example, if I push some new widget from portrait (let's say a login page).

Then if I rotate my device to landscape, it should inflate the login page UI in landscape mode but according to the code which pops all the widget until the root.

What I will see is the Landscape widget instead of the Login Page in landscape mode.

For clarity:

I am looking for an answer to close all open dialogs/pop-ups before it's parent widget is disposed. The solution should not be dependent on popping out the whole widget from the navigator.

Something that I found out from the widget tree representation is that the widgets that are shown as a popup (popup menu or dialog) are at a different branch of widget directly from the MaterialApp.

Check out these screenshots:

Visible Pop up menu in Landscape widget:

Pop up menu in Landscape widget

Visible Dialog in Landscape widget:

Dialog in Landscape widget

Visible Pop up menu in Portrait widget:

Pop up menu in Portrait widget

Visible Dialog in Portrait widget:

Dialog in Portrait widget

So, basically I am looking for a way to find and pop all these types of widgets that we can pop before disposing of the parent widget. I guess this should be applicable to all the widget screen and should not be altering the existing widget tree above the current widget.

Update 2

I got an answer which successfully removes the dialogs and popups but it has a side effect, which is that while removing all the dialogs/popups it also removes any widgets that are added on top of the root widget which showed the popups.

For example: in this example, consider a details page that I visit from the portrait widget. So when the portrait widget rebuilds the detail page is disposed and only portrait is shown again even though I do not have any dialog/popup open.

Harshvardhan Joshi
  • 2,855
  • 2
  • 18
  • 31

2 Answers2

6

As per the code given, I have changed the gesture detector on long press show dialog code and its child code as below:

// alert dialog code
...
builder: (context) => AlertDialog(
              content:
                  MediaQuery.of(context).orientation == Orientation.portrait
                      ? Text('Portrait')
                      : Text('Landscape'),
            ),
...

// gesture detector  child code

...
Center(
    child: MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
),
...

Output:
1

Conclusion: MediaQuery.of(context).orientation handles itself.

Update:
If you go with lifecycle while orientation change with this code, Only build method will be called not dispose method. You can remove all the popups while build method call.
Check out the code below.
Here on long-press, I have opened 3 popups for demo purpose(there can be any number of popups or menus)... While orientation changes, Navigator.of(context).popUntil((route) => route.isFirst); will be called first and pop all the popups and menus at first.

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return OrientationBuilder(builder: (context, orientation) {
      bool isLandscape = orientation == Orientation.landscape;
      return isLandscape ? Landscape() : Portrait();
    });
  }
}

class Portrait extends StatefulWidget {
  @override
  _PortraitState createState() => _PortraitState();
}

class _PortraitState extends State<Portrait> {
  // this method will not be called when orientation changes
//  @override
//  void dispose() {
//    super.dispose();
//    Navigator.pop(context);
//    print("Portrait dispose");
//  }

  @override
  Widget build(BuildContext context) {
    // the below line will pop all the popups
    Navigator.of(context).popUntil((route) => route.isFirst);
    // code to check this method is called when orientation is changed
    print("Portrait build");
    return Scaffold(
      appBar: AppBar(
        title: buildTitle(),
        actions: <Widget>[_buildOptionMenu(context)],
      ),
      body: GestureDetector(
        onLongPress: () {
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: Center(
                child:
                    MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
              ),
            ),
          );
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: Center(
                child:
                    MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
              ),
            ),
          );
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: Center(
                child:
                    MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
              ),
            ),
          );
        },
        child: Container(
          color: Colors.blue.withOpacity(0.4),
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Center(
                child: Center(
                  child:
                      MediaQuery.of(context).orientation == Orientation.portrait
                          ? Text('Portrait')
                          : Text('Landscape'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildOptionMenu(BuildContext context) {
    return PopupMenuButton(itemBuilder: (context) {
      var list = <String>['Portrait-Item-1', 'Portrait-Item-2'];
      return list
          .map<PopupMenuEntry<String>>(
            (e) => PopupMenuItem<String>(
              child: Text(e),
            ),
          )
          .toList();
    });
  }

  Text buildTitle() => Text('Portrait');
}

class Landscape extends StatefulWidget {
  @override
  _LandscapeState createState() => _LandscapeState();
}

class _LandscapeState extends State<Landscape> {
  // this method will not be called when orientation changes
//  @override
//  void dispose() {
//    super.dispose();
//    Navigator.pop(context);
//    print("Landscape dispose");
//  }

  @override
  Widget build(BuildContext context) {
    // the below line will pop all the popups
    Navigator.of(context).popUntil((route) => route.isFirst);
    // code to check this method is called when orientation is changed
    print("Landscape build");
    return Scaffold(
      appBar: AppBar(
        title: buildTitle(),
        actions: <Widget>[_buildOptionMenu(context)],
      ),
      body: GestureDetector(
        onLongPress: () {
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: Center(
                child:
                    MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
              ),
            ),
          );
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: Center(
                child:
                    MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
              ),
            ),
          );
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              content: Center(
                child:
                    MediaQuery.of(context).orientation == Orientation.portrait
                        ? Text('Portrait')
                        : Text('Landscape'),
              ),
            ),
          );
        },
        child: Container(
          color: Colors.orange.withOpacity(0.3),
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Center(
                child: Center(
                  child:
                      MediaQuery.of(context).orientation == Orientation.portrait
                          ? Text('Portrait')
                          : Text('Landscape'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Text buildTitle() => Text('Landscape');

  Widget _buildOptionMenu(BuildContext context) {
    return PopupMenuButton(itemBuilder: (context) {
      var list = <String>[
        'Landscape-Item-1',
        'Landscape-Item-2',
        'Landscape-Item-3',
      ];
      return list
          .map<PopupMenuEntry<String>>(
            (e) => PopupMenuItem<String>(
              child: Text(e),
            ),
          )
          .toList();
    });
  }
}

Output:
enter image description here

Sanket Vekariya
  • 2,848
  • 3
  • 12
  • 34
0

This is my solution for popping bottom sheets and dialogs in dispose method:

  NavigatorState _navigatorState;

  bool init = false;

  @override
  void didChangeDependencies() {
    if (!init) {
      final navigator = Navigator.of(context);
      setState(() {
        _navigatorState = navigator;
      });
      init = true;
    }
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    if(_isOpen) {
      _navigatorState.maybePop();
    }
    super.dispose();
  }

I just create a variable _isOpen that is updated when open the dialog/bottom sheets. That way I know when the dialog/bottom sheet is open, and if is currently open, close the dialog using maybePop().

The reason using maybePop() instead of pop() is because using pop() give us the the setState() or markNeedsBuild() called when widget tree was locked. error.

  • Thanks for your contribution to this issue. But it seems that for each screen I'll have to implement this "flag". Which is not developer friendly. I am looking for something in the flutter framework itself so that I can use it whenever I need it without additional coding. – Harshvardhan Joshi Dec 12 '20 at 05:31