55

I've been searching around for a good navigation/router example for Flutter but I have not managed to find one.

What I want to achieve is very simple:

  1. Persistent bottom navigation bar that highlights the current top level route
  2. Named routes so I can navigate to any route from anywhere inside the app
  3. Navigator.pop should always take me to the previous view I was in

The official Flutter demo for BottomNavigationBar achieves 1 but back button and routing dont't work. Same problem with PageView and TabView. There are many other tutorials that achieve 2 and 3 by implementing MaterialApp routes but none of them seem to have a persistent navigation bar.

Are there any examples of a navigation system that would satisfy all these requirements?

Janne Annala
  • 25,928
  • 8
  • 31
  • 41
  • did you find any solution for your question? – Daniel.V Apr 25 '19 at 10:05
  • @Daniel.V No I didn't. I ended up using BottomNavigationBar to achieve 1, ditched requirement no. 2 and manually handled pop so that I could always navigate back to the root view of the current tab. The open source scene has evolved since then so there might be new libraries that might achieve at least 1 and 2. – Janne Annala Apr 25 '19 at 12:22

3 Answers3

62

All of your 3 requirements can be achieved by using a custom Navigator.

The Flutter team did a video on this, and the article they followed is here: https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386

Basically, you will need to wrap the body of your Scaffold in a custom Navigator:

class _MainScreenState extends State<MainScreen> {
  final _navigatorKey = GlobalKey<NavigatorState>();

  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Navigator(
        key: _navigatorKey,
        initialRoute: '/',
        onGenerateRoute: (RouteSettings settings) {
          WidgetBuilder builder;
          // Manage your route names here
          switch (settings.name) {
            case '/':
              builder = (BuildContext context) => HomePage();
              break;
            case '/page1':
              builder = (BuildContext context) => Page1();
              break;
            case '/page2':
              builder = (BuildContext context) => Page2();
              break;
            default:
              throw Exception('Invalid route: ${settings.name}');
          }
          // You can also return a PageRouteBuilder and
          // define custom transitions between pages
          return MaterialPageRoute(
            builder: builder,
            settings: settings,
          );
        },
      ),
      bottomNavigationBar: _yourBottomNavigationBar,
    );
  }
}

Within your bottom navigation bar, to navigate to a new screen in the new custom Navigator, you just have to call this:

_navigatorKey.currentState.pushNamed('/yourRouteName');

To achieve the 3rd requirement, which is Navigator.pop taking you to the previous view, you will need to wrap the custom Navigator with a WillPopScope:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: WillPopScope(
      onWillPop: () async {
        if (_navigatorKey.currentState.canPop()) {
          _navigatorKey.currentState.pop();
          return false;
        }
        return true;
      },
      child: Navigator(
        // ...
      ),
    ),
    bottomNavigationBar: _yourBottomNavigationBar,
  );
}

And that should be it! No need to manually handle pop or manage a custom history list.

CZX
  • 1,977
  • 1
  • 14
  • 19
  • How to keep alive every tab. In above method each page is recreated when move to next tab/bottomNavigationItem. – Abdullah Khan Feb 20 '20 at 09:18
  • 2
    @AbdullahKhan You shouldn't need to. Just separate state from UI. Some useful library options: Redux, BLoC, MobX. – Janne Annala Mar 27 '20 at 07:35
  • 3
    @czx How to hide the bottom navigation bar in sub routes? – Lakshan Mamalgaha Jun 12 '20 at 05:31
  • Is there a way that you could also use this from the very top level? I use flutter for web and this kind of navigation does not work from the url tab sadly. – Paul Groß Oct 08 '20 at 12:04
  • @CZX I don't really get it. This solution implies that Homepage(), Page1() and Page2() must not return Scaffolds themselves because one does not want to have nested Scaffolds. But what if my Widgets return Scaffolds, e.g. because they have Floating Action Button? – Schnodderbalken Aug 17 '21 at 17:09
  • This works, but for some reason I get massive lag from it., has anyone else experience this? – joshpetit Aug 17 '21 at 20:50
  • 1
    `No need to manually handle pop or manage a custom history list` - it's need, becasue bottom nav has currentIndex and you need manage it on every pop, push. – ivanbogomolov Sep 24 '21 at 09:39
  • @Janne Annala I think he means that `builder = newPage()` will create a new page on every switch, since it's calling the constructor – MwBakker May 04 '22 at 00:26
6

CupertinoTabBar behave exactly same as you described, but in iOS style. It can be used in MaterialApps however.

Sample Code

Kyaw Tun
  • 12,447
  • 10
  • 56
  • 83
  • The link to CupertinoTabBar is 404. Try this instead: https://api.flutter.dev/flutter/cupertino/CupertinoTabBar-class.html – ijt Jan 18 '23 at 21:28
4

What you are asking for would violate the material design specification.

On Android, the Back button does not navigate between bottom navigation bar views.

A navigation drawer would give you 2 and 3, but not 1. It depends on what's more important to you.

You could try using LocalHistoryRoute. This achieves the effect you want:

class MainPage extends StatefulWidget {
  @override
  State createState() {
    return new MainPageState();
  }
}

class MainPageState extends State<MainPage> {
  int _currentIndex = 0;
  List<int> _history = [0];

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Bottom Nav Back'),
      ),
      body: new Center(
        child: new Text('Page $_currentIndex'),
      ),
      bottomNavigationBar: new BottomNavigationBar(
        currentIndex: _currentIndex,
        items: <BottomNavigationBarItem>[
          new BottomNavigationBarItem(
            icon: new Icon(Icons.touch_app),
            title: new Text('keypad'),
          ),
          new BottomNavigationBarItem(
            icon: new Icon(Icons.assessment),
            title: new Text('chart'),
          ),
          new BottomNavigationBarItem(
            icon: new Icon(Icons.cloud),
            title: new Text('weather'),
          ),
        ],
        onTap: (int index) {
          _history.add(index);
          setState(() => _currentIndex = index);
          Navigator.push(context, new BottomNavigationRoute()).then((x) {
            _history.removeLast();
            setState(() => _currentIndex = _history.last);
          });
        },
      ),
    );
  }
}

class BottomNavigationRoute extends LocalHistoryRoute<void> {}
Richard Heap
  • 48,344
  • 9
  • 130
  • 112
  • 13
    This is something I want to achieve even if it violates some arbitrary design rule. Even the official Youtube app navigate between bottom nav bar views when pressing the back button. Same goes for Spotify and pretty much all big apps out there. – Janne Annala Apr 12 '18 at 19:02
  • Each tap push a Navigation stack, but never removed. – Kyaw Tun May 22 '18 at 08:07
  • They are removed by the Android back button, as requested by the question. – Richard Heap May 22 '18 at 11:31
  • 1
    I got it working by extending `Route` instead of `LocalHistoryRoute` – HTMHell Jul 22 '19 at 22:42
  • On Android, hitting the back button in an activity containing a BottomNavigationBar should pop to the first index (first page) & hitting the back button again should finish the activity. – Brendan Apr 30 '20 at 02:50