20

Does anyone have any recommendations for figuring out nested navigation in Flutter?

What I want is to keep a persistent BottomNavigationBar even when redirecting to new screens. Similar to YouTube, where the bottom bar is always there, even when you browse deeper into the menus.

I'm unable to figure it out from the docs.

The only tutorial I have been able to find so far that goes in-depth into exactly my requirement is https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf (source code). However, It's super confusing.

Right now I'm using

Navigator.push(context,
                MaterialPageRoute(builder: (BuildContext context) {
              return Container()

However, its just pushing the new widget over the entire stack, covoring the BottomNavigationBar.

Any tips would be greatly appreciated!

Moshe G
  • 516
  • 1
  • 4
  • 13

2 Answers2

30

Here is a simple example that even supports popping to the first screen with a tab bar.

import 'package:flutter/material.dart';

import '../library/screen.dart';
import '../playlists/screen.dart';
import '../search/screen.dart';
import '../settings/screen.dart';

class TabsScreen extends StatefulWidget {
  @override
  _TabsScreenState createState() => _TabsScreenState();
}

class _TabsScreenState extends State<TabsScreen> {
  int _currentIndex = 0;

  final _libraryScreen = GlobalKey<NavigatorState>();
  final _playlistScreen = GlobalKey<NavigatorState>();
  final _searchScreen = GlobalKey<NavigatorState>();
  final _settingsScreen = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: <Widget>[
          Navigator(
            key: _libraryScreen,
            onGenerateRoute: (route) => MaterialPageRoute(
              settings: route,
              builder: (context) => LibraryScreen(),
            ),
          ),
          Navigator(
            key: _playlistScreen,
            onGenerateRoute: (route) => MaterialPageRoute(
              settings: route,
              builder: (context) => PlaylistsScreen(),
            ),
          ),
          Navigator(
            key: _searchScreen,
            onGenerateRoute: (route) => MaterialPageRoute(
              settings: route,
              builder: (context) => SearchScreen(),
            ),
          ),
          Navigator(
            key: _settingsScreen,
            onGenerateRoute: (route) => MaterialPageRoute(
              settings: route,
              builder: (context) => SettingsScreen(),
            ),
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        currentIndex: _currentIndex,
        onTap: (val) => _onTap(val, context),
        backgroundColor: Theme.of(context).scaffoldBackgroundColor,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.library_books),
            title: Text('Library'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            title: Text('Playlists'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            title: Text('Search'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            title: Text('Settings'),
          ),
        ],
      ),
    );
  }

  void _onTap(int val, BuildContext context) {
    if (_currentIndex == val) {
      switch (val) {
        case 0:
          _libraryScreen.currentState.popUntil((route) => route.isFirst);
          break;
        case 1:
          _playlistScreen.currentState.popUntil((route) => route.isFirst);
          break;
        case 2:
          _searchScreen.currentState.popUntil((route) => route.isFirst);
          break;
        case 3:
          _settingsScreen.currentState.popUntil((route) => route.isFirst);
          break;
        default:
      }
    } else {
      if (mounted) {
        setState(() {
          _currentIndex = val;
        });
      }
    }
  }
}

Rody Davis
  • 1,825
  • 13
  • 20
  • 1
    This worked very well for me - I [generalized this approach](https://gist.github.com/micimize/a4ae34ae4f551bab94ac564dd18d74a9) and added some helpers like `tabNavigatorOf(context, { int tabIndex })` – micimize Aug 31 '19 at 23:15
  • I was having an issue in keeping the tab state but indexed stack worked perfectly thanks – Tarek Badr Jan 04 '20 at 14:58
  • 5
    After doing this, how can we push a page without the bottom bar from any inner page? – nazaif Jul 25 '20 at 06:52
  • 1
    I'm having troubles with this approach - when I switch from one tab to another and then hit the back button, the app closes instead of switching back to the first tab. Also, if I do a nested navigation, e.g. switch to another page with the navigator inside e.g. the Library page, hitting back also closes the app. Any tips? – schneida Apr 16 '21 at 14:31
  • This approach causes the issue is that tapping the status bar doesn't scroll to top. – Tuan van Duong May 13 '21 at 13:26
  • 1
    if button key switching back to first page try to use WillPopScope on navigators page with key value.. @schneida – JahidRatul Nov 01 '21 at 07:30
  • can you please tell me how to use this navigation in `pushAndRemoveUntil` ...it will be very helpful. Actually i am using awesome_notification package – MNBWorld Feb 27 '22 at 17:50
  • Thx. This is something unreal - all tutorial provides too much information instead one worful and clear example – Georgiy Chebotarev Mar 28 '23 at 08:43
7

Here is the example code for persistent BottomNavigationBar as a starter:

import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MainPage(),
    );
  }
}

class MainPage extends StatelessWidget {
  final navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            child: Navigator(
              key: navigatorKey,
              onGenerateRoute: (route) => MaterialPageRoute(
                    settings: route,
                    builder: (context) => PageOne(),
                  ),
            ),
          ),
          BottomNavigationBar(navigatorKey)
        ],
      ),
    );
  }
}

class BottomNavigationBar extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey;

  BottomNavigationBar(this.navigatorKey) : assert(navigatorKey != null);

  Future<void> push(Route route) {
    return navigatorKey.currentState.push(route);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: ButtonBar(
        alignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          RaisedButton(
            child: Text("PageOne"),
            onPressed: () {
              push(MaterialPageRoute(builder: (context) => PageOne()));
            },
          ),
          RaisedButton(
            child: Text("PageTwo"),
            onPressed: () {
              push(MaterialPageRoute(builder: (context) => PageTwo()));
            },
          )
        ],
      ),
    );
  }
}

class PageOne extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("Page One"),
          RaisedButton(
            onPressed: (){
              Navigator.of(context).pop();
            },
            child: Text("Pop"),
          ),
        ],
      ),
    );
  }
}

class PageTwo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("Page Two"),
          RaisedButton(
            onPressed: (){
              Navigator.of(context).pop();
            },
            child: Text("Pop"),
          ),
        ],
      ),
    );
  }
}

Here is how it the screen record

Screen Record of Nested Navigator for persistent Bottom Navigation Bar

Harsh Bhikadia
  • 10,095
  • 9
  • 49
  • 70
  • 1
    I tried something based on your solution. I created an `Expanded` `Column` in the `Scaffold` body. I put a `BottomNavigationBar` inside and I'm using a `GlobalKey()` to push new pages. However, the animations between `BottomNavigationBar` pages are extremely janky. It's not using the `BottomNavigationBar` animation. It's just stacking one page on top of the other (and messing up the `go back`) – Moshe G Apr 17 '19 at 01:36
  • I'm not understanding, what animation you want? Slide Left-right transition? – Harsh Bhikadia Apr 17 '19 at 01:41
  • @HarshBhikadia I tried your solution and works well but, when on a specific page, e.g. Page1, and Page1 has a TextField() Widget, when clicking on that TextField widget, it automatically opens the default route which is "/". Basically, it reopens the page on which that default route returns. Any way to solve this? – Jama Mohamed Jul 05 '19 at 11:19
  • 1
    It seems the nested Navigator loses its state when hot reloading and i start from the initial route. Is anyone else having this behaviour? Its pretty anoying when developing... – Jonas Apr 13 '20 at 20:29
  • @HarshBhikadia After doing like this, if I want to not show the bottom bar from an inner page(like a detail page), how do i achieve this? – nazaif Jul 28 '20 at 16:25
  • Ideally we shouldn't need any stack or global keys. Flutter should create a widget or a functionality for this. – Iván Yoed Feb 17 '21 at 14:47