0

I want to close my Drawer widget every time the user presses a button in the Bottom Navigation Bar, but can't quite figure this thing out. The way my setup of the BNB is set now is that the current state of all screen is remembered through out the app (using an IndexedStack), but I want to close the Drawer if it is opened in any of the screens before the BNB button press. Each of my screens have their own Drawers and AppBars, so I can't make one Drawer inside the BNB (or I can and I can dynamically change them with a switch case when a specific Screen is clicked on BUT then the Drawer will cover the Bottom Navigation Bar etc.), but I want to make it work like this for now. So here is the code with some comments inside to explain things:

Bottom Navigation Bar:

class BottomNavBar extends StatefulWidget {
  static const String id = 'bottom_navbar_screen';
  @override
  _BottomNavBarState createState() => _BottomNavBarState();
}

class _BottomNavBarState extends State<BottomNavBar> {
  int _selectedIndex = 0;

  /// list of screen that will render inside the BNB
  List<Navigation> _items = [
    Navigation(
        widget: Screen1(), navigationKey: GlobalKey<NavigatorState>()),
    Navigation(
        widget: Screen2(), navigationKey: GlobalKey<NavigatorState>()),
    Navigation(
        widget: Screen3(), navigationKey: GlobalKey<NavigatorState>()),
    Navigation(
        widget: Screen4(), navigationKey: GlobalKey<NavigatorState>()),
  ];

  /// function that renders components based on selected one in the BNB

  void _onItemTapped(int index) {
    if (index == _selectedIndex) {
      _items[index]
          .navigationKey
          .currentState
          .popUntil((route) => route.isFirst);
    } else {
      setState(() {
        _selectedIndex = index;
      });
    }
 /// when the index is selected, on the button press do some actions
    switch (_selectedIndex) {
      case 0:
         //  Do some actions
        break;
      case 1:
         //  Do some actions
        break;
      case 2:
         //  Do some actions
        break;
      case 3:
         //  Do some actions
        break;
    }
  }

  /// navigation Tab widget for a list of all the screens and puts them in a Indexed Stack
  Widget _navigationTab(
      {GlobalKey<NavigatorState> navigationKey, Widget widget}) {
    return Navigator(
      key: navigationKey,
      onGenerateRoute: (routeSettings) {
        return MaterialPageRoute(builder: (context) => widget);
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        final isFirstRouteInCurrentTab =
            !await _items[_selectedIndex].navigationKey.currentState.maybePop();
        if (isFirstRouteInCurrentTab) {
          if (_selectedIndex != 0) {
            _onItemTapped(1);
            return false;
          }
        }

        /// let system handle back button if we're on the first route
        return isFirstRouteInCurrentTab;
      },
      child: Scaffold(
        body: IndexedStack(
          index: _selectedIndex,
          children: _items
              .map((e) => _navigationTab(
                  navigationKey: e.navigationKey, widget: e.widget))
              .toList(),
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              label: 'Screen 1,
            ),
            BottomNavigationBarItem(
              label: 'Screen 2,
            ),
            BottomNavigationBarItem(
              label: 'Screen 3,
            ),
            BottomNavigationBarItem(
              label: 'Screen 4,
            ),
          ],
          currentIndex: _selectedIndex,
          showUnselectedLabels: true,
          onTap: _onItemTapped,
        ),
      ),
    );
  }
}

Let's say all 4 screens are the same and they have their own AppBar and Drawer:

@override
  Widget build(BuildContext context) {
    return  Scaffold(
        backgroundColor: Colors.white,
        drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
        appBar: AppBar( // each screen has its own app bar
          title: Text('Screens 1-4'),
        ),
        body: Text('Body of Screens 1-4'),
    );
  }

So because each of the screens have their own AppBars and Drawers, the Drawer doesn't render over the Bottom Navigation Bar, so my BNB buttons can be clicked. If I put one Drawer for all Screens inside the BNB, then you can't click the BNB unless you close the Drawer first, which is not something I'm looking for right now.

So, my final question is, how do I close each of the Screens Drawers (if they are previously opened that is) when you press the BottomnavigationBar? (i.e. I am on Screen 1, I open the Drawer, then I press on Screen 2 in the BNB and I want to pop()/close the Drawer in Screen 1 before I navigate to Screen 2.)

Thanks in advance for your help!

GrandMagus
  • 600
  • 3
  • 12
  • 37
  • 1
    Maybe [this](https://stackoverflow.com/questions/43807184/how-to-close-scaffolds-drawer-after-an-item-tap) helps you, check the answer containing `ScaffoldState`. – Peter Koltai Jan 20 '22 at 13:43
  • Thanks @PeterKoltai but this is when you want to click an item inside the Drawer, but I want it to close when I tap the BottomNavigationBar, I just open the Drawer and then press the BNB, no actions inside the Drawer are done. – GrandMagus Jan 20 '22 at 14:04
  • 1
    I see. Maybe I get it wrong but it seems that you can get whether the drawer of `Scaffold` is open with [this](https://api.flutter.dev/flutter/material/ScaffoldState/isDrawerOpen.html) and use `Navigator.pop()` to close drawer when it is open. – Peter Koltai Jan 20 '22 at 15:21
  • Actually you were right @PeterKoltai so after reading upon the docs a bit more and with some help from jeremynac I found a solution. Thanks again mate! – GrandMagus Jan 26 '22 at 10:43
  • Good to hear, you are welcome! – Peter Koltai Jan 26 '22 at 11:55

1 Answers1

2

A good way to do this is to use a GlobalKey for your scaffold. So, for all your scaffolds, you define them using:

class SomeClass extends StatelessWidget {
  final scaffoldKey = GlobalKey<ScaffoldState>()

  Widget build(BuildContext context) {
    Scaffold(
      backgroundColor: Colors.white,
      drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
      appBar: AppBar( // each screen has its own app bar
        title: Text('Screens 1-4),
      ),
      body: Text('Body of Screens 1-4),
      key: scaffoldKey,
      ),
    );
  }

}

And then, you can pass this key to your BottomNavigationBar. In your BottomNavigationBar, you can have all the scaffoldKeys, and in the onItemTap function:

void _onItemTapped(int index) {
    for (scaffoldKey in scaffoldKeys) {
      // If the drawer is open
      if (scaffoldKey.currentState.isDrawerOpen) {
        // Closes the drawer
        scaffoldKey.currentState?.openEndDrawer();
      }
    }
    if (index == _selectedIndex) {
      _items[index]
          .navigationKey
          .currentState
          .popUntil((route) => route.isFirst);
    } else {
      setState(() {
        _selectedIndex = index;
      });
    }
 /// when the index is selected, on the button press do some actions
    switch (_selectedIndex) {
      case 0:
         //  Do some actions
        break;
      case 1:
         //  Do some actions
        break;
      case 2:
         //  Do some actions
        break;
      case 3:
         //  Do some actions
        break;
    }
  }

It's up to you to find the best way of passing around the keys. You could for example define them in a Widgets that contains both the bottom navigation bar and the different scaffolds, and pass it down as parameters. You could use State Management... whatever fits your use case.

Here is what your code could look like:

class BottomNavBar extends StatefulWidget {
  static const String id = 'bottom_navbar_screen';
  @override
  _BottomNavBarState createState() => _BottomNavBarState();
}

class _BottomNavBarState extends State<BottomNavBar> {
  int _selectedIndex = 0;
  late final List<GlobalKey<ScaffoldState>> scaffoldKeys;
  /// list of screen that will render inside the BNB
  late final List<Navigation> _items;
  @override
  initState() {
  super.initState()
  scaffoldKeys = [GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>()];
  _items = [
    Navigation(
        widget: Screen1(scaffoldKey: scaffoldKeys[0]), navigationKey: GlobalKey<NavigatorState>()),
    Navigation(
        widget: Screen2(scaffoldKey: scaffoldKeys[1]), navigationKey: GlobalKey<NavigatorState>()),
    Navigation(
        widget: Screen3(scaffoldKey: scaffoldKeys[2]), navigationKey: GlobalKey<NavigatorState>()),
    Navigation(
        widget: Screen4(scaffoldKey: scaffoldKeys[3]), navigationKey: GlobalKey<NavigatorState>()),
  ];
  }

  /// function that renders components based on selected one in the BNB

  void _onItemTapped(int index) {
    for (scaffoldKey in scaffoldKeys) {
      // If the drawer is open
      if (scaffoldKey.currentState.isDrawerOpen) {
        // Closes the drawer
        scaffoldKey.currentState?.openEndDrawer();
      }
    }
    if (index == _selectedIndex) {
      _items[index]
          .navigationKey
          .currentState
          .popUntil((route) => route.isFirst);
    } else {
      setState(() {
        _selectedIndex = index;
      });
    }
 /// when the index is selected, on the button press do some actions
    switch (_selectedIndex) {
      case 0:
         //  Do some actions
        break;
      case 1:
         //  Do some actions
        break;
      case 2:
         //  Do some actions
        break;
      case 3:
         //  Do some actions
        break;
    }
  }

  /// navigation Tab widget for a list of all the screens and puts them in a Indexed Stack
  Widget _navigationTab(
      {GlobalKey<NavigatorState> navigationKey, Widget widget, GlobalKey<ScaffoldState> scaffoldKey}) {
    return Navigator(
      key: navigationKey,
      onGenerateRoute: (routeSettings) {
        return MaterialPageRoute(builder: (context) => widget);
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        final isFirstRouteInCurrentTab =
            !await _items[_selectedIndex].navigationKey.currentState.maybePop();
        if (isFirstRouteInCurrentTab) {
          if (_selectedIndex != 0) {
            _onItemTapped(1);
            return false;
          }
        }

        /// let system handle back button if we're on the first route
        return isFirstRouteInCurrentTab;
      },
      child: Scaffold(
        body: IndexedStack(
          index: _selectedIndex,
          children: _items
              .map((e) => _navigationTab(
                  navigationKey: e.navigationKey, widget: e.widget))
              .toList(),
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              label: 'Screen 1,
            ),
            BottomNavigationBarItem(
              label: 'Screen 2,
            ),
            BottomNavigationBarItem(
              label: 'Screen 3,
            ),
            BottomNavigationBarItem(
              label: 'Screen 4,
            ),
          ],
          currentIndex: _selectedIndex,
          showUnselectedLabels: true,
          onTap: _onItemTapped,
        ),
      ),
    );
  }
}

And you screens:

class Screen1 extends StatelessWidget {
  final GlobalKey<ScaffoldState> scaffoldKey;
  Screen1({required this.scaffoldKey});
  @override
  Widget build(BuildContext context) {
    return  Scaffold(
        key: scaffoldKey,
        backgroundColor: Colors.white,
        drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
        appBar: AppBar( // each screen has its own app bar
          title: Text('Screens 1-4'),
        ),
        body: Text('Body of Screens 1-4'),
    );
    }
  }

I changed the list of screens _items to a late variables so you can pass the scaffoldKeys to them when declaring them.

jeremynac
  • 1,204
  • 3
  • 11
  • Isn’t openEndDrawer() kind of a hack? Thanks for the help though. – GrandMagus Jan 23 '22 at 10:20
  • Not really, it's basically used to close or open a scaffold's drawer programmatically. That's pretty much the only way I know of to close a drawer without using the context (which would definitely be a hack in your case as you would have to gather 4 different contexts) – jeremynac Jan 23 '22 at 10:45
  • 1
    You can see what openEndDrawer() does here https://api.flutter.dev/flutter/material/ScaffoldState/openEndDrawer.html – jeremynac Jan 23 '22 at 10:45
  • 1
    The only tricky part about openEndDrawer is that it works like the drawer button of the Scaffold, meaning it will open the drawer if it's closed and close it if it's opened. But you just have to check wether the drawer is opened to avoid that problem – jeremynac Jan 23 '22 at 10:47
  • Ok, but how am I going to pass the scaffoldKeys to my BNB when my current set up is a `IndexedStack`, that just 'switches' between screens when I press on the BNB menu? I never pass anything to the BNB, I only use BNB to navigate between the desired screens. – GrandMagus Jan 23 '22 at 11:14
  • So I read upon the documentation and came back to tell you I found a solution then I realized you updated the answer.. Thanks a lot for the help mate! I'm a little rusty because it's been a while but, merci beaucoup mon frere. :) I have awarded the +50 rep to you for your answer! – GrandMagus Jan 26 '22 at 10:40