Flutter: Keep BottomNavigationBar When Push to New Screen with Navigator

53,578

Solution 1

tl;dr: Use CupertinoTabBar with CupertinoTabScaffold

The problem is not in Flutter but in UX just like Rémi Rousselet has mentioned.

It turned out Material Design doesn't recommend sub-pages in the hierarchy to access the Bottom navigation bar.

However, iOS Human Interface Guide recommend this. So, to use this feature, I had to adapt Cupertino widgets instead of Material ones. Specifically, in main, return a WidgetsApp/MaterialApp which contains a CupertinoTabScaffold. Implement the tab bar with a CupertinoTabBar and each screen is a CupertinoTabView.

Solution 2

Screenshot:

enter image description here


Starting point:

void main() => runApp(MaterialApp(home: HomePage()));

HomePage [BottomNavigationBar + Page1]

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        backgroundColor: Colors.orange,
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.call), label: 'Call'),
          BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Message'),
        ],
      ),
      body: Navigator(
        onGenerateRoute: (settings) {
          Widget page = Page1();
          if (settings.name == 'page2') page = Page2();
          return MaterialPageRoute(builder: (_) => page);
        },
      ),
    );
  }
}

1st Page:

class Page1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Page1')),
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.pushNamed(context, 'page2'),
          child: Text('Go to Page2'),
        ),
      ),
    );
  }
}

2nd Page:

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}

Solution 3

You could actually place a placeholder inside body so the structure like this

- AppBar
- body (dynamic content from placeholder)
- BottomNavigationBar

Then you would have another class as a placeholder So each time you tap on the BottomNavigationBar it will refresh content of the body

One example I found is here https://willowtreeapps.com/ideas/how-to-use-flutter-to-build-an-app-with-bottom-navigation

and here but a litte too complex and not working for me https://medium.com/@swav.kulinski/flutter-navigating-off-the-charts-e118562a36a5

and this https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf

Solution 4

Option 1: If you only want to keep BottomNavigationBar then try to use this.

Option 2: Use CupertinoTabBar as shown below for the static BottomNavigationBar.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mqttdemo/Screen2.dart';
import 'package:mqttdemo/Screen3.dart';

import 'Screen1.dart';

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _currentIndex;
  List<Widget> _children;

  @override
  void initState() {
    _currentIndex = 0;
    _children = [
      Screen1(),
      Screen2(),
      Screen3(),
    ];
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        currentIndex: _currentIndex,
        onTap: onTabTapped,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text("Screen 1"),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text("Screen 2"),
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.home), title: Text("Screen 3")),
        ],

      ),
        tabBuilder: (BuildContext context, int index) {
          return CupertinoTabView(
            builder: (BuildContext context) {
              return SafeArea(
                top: false,
                bottom: false,
                child: CupertinoApp(
                  home: CupertinoPageScaffold(
                    resizeToAvoidBottomInset: false,
                    child: _children[_currentIndex],
                  ),
                ),
              );
            },
          );
        }
    );
  }

  void onTabTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }
}

Navigate to screen4 from Screen3 as shown below:

    class Screen3 extends StatefulWidget {
      @override
      _Screen3State createState() => _Screen3State();
    }
    
    class _Screen3State extends State<Screen3> {
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.black,
          child: Center(
            child: RaisedButton(
              child: Text("Click me"),
              onPressed: () {
                Navigator.of(context, rootNavigator: false).push(MaterialPageRoute(
                    builder: (context) => Screen4(), maintainState: false));
              },
            ),
          ),
        );
      }

}

Solution 5

Another way to achieve this (though not good practice) is to nest a material app in the body of your scaffold. And handle all "sub-navigation" there.

So, your hierarchy will look like this

Material App
  - home
     - Scaffold
       - body
         - Material App
              - Scaffold
                  - AppBar
                  - body
                  ...
         - routes (internal)
       - bottomNavigationBar
  - routes (external)

I've tried this and it works perfectly. Unfortunately I can't post the source code now.

Share:
53,578
harry
Author by

harry

Updated on July 29, 2022

Comments

  • harry
    harry almost 2 years

    In iOS, we have a UITabBarController which stays permanently at the bottom of the screen when we push to a new ViewController.

    In Flutter, we have a bottomNavigationBar of a Scaffold. However, unlike iOS, when we Navigator.push to a new screen, this bottomNavigationBar disappears.

    In my app, I want to fulfil this requirement: Home screen has a bottomNavigationBar with 2 items (a & b) presenting screen A & B. By default, screen A is displayed. Inside screen A, there is a button. Tap that button, Navigator.push to screen C. Now in screen C, we can still see the bottomNavigationBar. Tap item b, I go to screen B. Now in screen B, tap item a in the bottomNavigationBar, I go back to screen C (not A, A is currently below C in the navigation hierarchy).

    How can I do this? Thanks, guys.

    Edit: I'm including some pictures for demonstration:

    Screen A Screen A

    Tap Go to C button, push to screen C Screen C

    Tap Right item inside bottom navigation bar, go to screen B Screen B

  • giorgio79
    giorgio79 almost 4 years
    Material Design now recommends navigation in sub-pages, as per github.com/flutter/flutter/issues/16221 and material.io/components/bottom-navigation#behavior It is just not yet implemented...
  • ThomasThiebaud
    ThomasThiebaud over 3 years
    Works like a charm. Thanks a lot
  • Guilherme Ferreira
    Guilherme Ferreira over 3 years
    Could not find a generator for route RouteSettings
  • MwBakker
    MwBakker about 3 years
    is it now implemented? I can still only find hacky workarounds or using 3rd party packages
  • Waqas Younis
    Waqas Younis about 3 years
    And how to navigate some screens without bottom navigation bar, could you explain that too?
  • batangaming
    batangaming about 3 years
    You can use root navigator: Navigator.of(context, rootNavigator: true).pushFunction(...) but you also need to configure routes in root, e.g set routes property in materialApp
  • Lakshmi Narayanan
    Lakshmi Narayanan almost 3 years
    @batangaming your comment just saved my day Thanks man
  • CopsOnRoad
    CopsOnRoad over 2 years
    @CNK Any references for "not recommended"?
  • KunYu Tsai
    KunYu Tsai over 2 years
    But how to navigate between different tabs with this solution?
  • CopsOnRoad
    CopsOnRoad over 2 years
    @KunYuTsai Add the onChanged property to the BottomNavigationBar widget, use an index to keep track of the current page and then use an if-else condition, for example, to choose which widget to show.
  • ladookie
    ladookie over 2 years
    @CopsOnRoad do you mean onTap? the onChanged property doesn't exist on BottomNavigationBar
  • CopsOnRoad
    CopsOnRoad over 2 years
    @ladookie Yeah, it's onTap, I think it was onChanged in earlier Flutter versions.
  • Mithson
    Mithson over 2 years
    @CopsOnRoad can you please provide a sample code for what kunYu Tsai had problem I am too facing the same issue but not able to make it work with Stateful widget
  • LearnFlutter
    LearnFlutter about 2 years
    Hi, I'm having the same issue, I have posted the question if you don't mind looking at it. @CopsOnRoad stackoverflow.com/questions/71484514/…
  • LearnFlutter
    LearnFlutter about 2 years
    Having the same issue on my case if you don't mind taking a look at it stackoverflow.com/questions/71484514/…
  • Krahmal
    Krahmal about 2 years
    Is is possible to pop back from page2 to page1?