Flutter: Keep BottomNavigationBar When Push to New Screen with Navigator
Asked Answered
A

12

60

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

Abuse answered 3/4, 2018 at 11:5 Comment(6)
Are the button you talked about inside BottomNavigationBar ?Caterwaul
No, the button is not in the bottom bar. It's inside main screen. It's just something to trigger Navigator to push to a new screen.Abuse
I think it's more a UX problem. As your C view shouldn't have the bottomnavigationbar. Or alternatively C should be accessible from that bottombar.Caterwaul
Isn't it common for screen in same hierarchy to be able to have that bottom navigation bar. Take Twitter for example (Please open the iOS Twitter app), tap on a tweet, a TweetViewController get pushed and the bottom bar is still visible. I think almost all popular apps have this behaviour.Abuse
I'd agree with Harry that this is quite a common thing in iOS, and TBH flutter's way of animating the entire screen is actually a little bit contrary to iOS's way of handling the navigation bar - although iOS does cover the navigation bar when it does modal popup type screens.Hallucinogen
using persistent_bottom_nav_bar package you can maintain a navigation route of individual tabs and BottomNavigationBar will not disappear when users navigate too to any screen.Efflux
A
34

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.

Abuse answered 4/4, 2018 at 9:38 Comment(2)
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...Philologian
is it now implemented? I can still only find hacky workarounds or using 3rd party packagesJany
S
50

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')));
}
Sarcocarp answered 11/1, 2021 at 11:27 Comment(14)
Works like a charm. Thanks a lotRhines
Could not find a generator for route RouteSettingsBanksia
And how to navigate some screens without bottom navigation bar, could you explain that too?Kr
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 materialAppConga
@Conga your comment just saved my day Thanks manTyishatyke
@CNK Any references for "not recommended"?Sarcocarp
But how to navigate between different tabs with this solution?Redbug
@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.Sarcocarp
@Sarcocarp do you mean onTap? the onChanged property doesn't exist on BottomNavigationBarChincapin
@Chincapin Yeah, it's onTap, I think it was onChanged in earlier Flutter versions.Sarcocarp
@Sarcocarp 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 widgetSloshy
Hi, I'm having the same issue, I have posted the question if you don't mind looking at it. @Sarcocarp #71485014Echeverria
Is is possible to pop back from page2 to page1?Sampson
@Sarcocarp can you please give example of navigating among pages in the BottomNavBar. using popUntil emits the BottomNavBarDiaster
A
34

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.

Abuse answered 4/4, 2018 at 9:38 Comment(2)
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...Philologian
is it now implemented? I can still only find hacky workarounds or using 3rd party packagesJany
M
11

If someone want to keep bottomNavigationBar, Just return a Navigator from body of home page and pass a unique key for each route.

import 'package:bottomnavigation_sample/page1.dart';
import 'package:bottomnavigation_sample/page2.dart';
import 'package:bottomnavigation_sample/page3.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 0;
  Map<int, GlobalKey<NavigatorState>> navigatorKeys = {
     0: GlobalKey<NavigatorState>(),
     1: GlobalKey<NavigatorState>(),
     2: GlobalKey<NavigatorState>(),
  };
    final List<Widget> _widgetOptions = <Widget>[
    const page1(),
    const page2(),
    const page3()
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
       bottomNavigationBar: BottomNavigationBar(
         items: const <BottomNavigationBarItem>[
           BottomNavigationBarItem(
             icon: Icon(Icons.home),
             label: 'Home',
           ),
           BottomNavigationBarItem(
             icon: Icon(Icons.business),
             label: 'Business',
           ),
           BottomNavigationBarItem(
             icon: Icon(Icons.school),
             label: 'School',
           ),
         ],
         currentIndex: _selectedIndex,
         selectedItemColor: Colors.amber[800],
         onTap: _onItemTapped,
       ),
      body:  buildNavigator(),
    );
  }

   buildNavigator() {
     return Navigator(
       key: navigatorKeys[_selectedIndex],
       onGenerateRoute: (RouteSettings settings){
         return MaterialPageRoute(builder: (_) => _widgetOptions.elementAt(_selectedIndex));
       },
     );
  }
}
Mohammed answered 2/8, 2022 at 7:10 Comment(0)
F
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));
              },
            ),
          ),
        );
      }

}
Flummery answered 5/5, 2020 at 13:20 Comment(0)
A
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

Acroter answered 30/11, 2018 at 7:50 Comment(0)
E
3

You can create Navigator widget in a Stack widget to use BottomNavigationBar with tabs' inner navigation. You can use WillPopScope to handle Android's back button to pop inner screens of tab. Also, double tap bottom navigation item to pop all inner screens of a tab.

I've created a Sample app for this.

Hope this help!

Entrenchment answered 28/4, 2019 at 9:43 Comment(0)
T
2

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.

Talent answered 31/8, 2018 at 11:12 Comment(1)
Having the same issue on my case if you don't mind taking a look at it #71485014Echeverria
H
1

I think the #right way of doing this would be to have the BottomNavigationBar wrapped in a Hero in both cases with the same tag. This way, when the animation between pages happens they would be excluded.

This is as brief as an example as I could make, but I'd highly recommend cleaning it up i.e. passing the hero string in, using widgets rather than a huge block of build, making your own widget for BottomNavigationBar.

Note that during the hero transition it does overflow by 0.0000191 pixels on my phone at least, but in release mode that shouldn't be an issue I don't think.

import 'package:flutter/material.dart';

void main() => runApp(new MaterialApp(
      home: new Builder(
        builder: (context) => new Scaffold(
              bottomNavigationBar: new Hero(
                tag: "bottomNavigationBar",
                child: new BottomNavigationBar(items: [
                  new BottomNavigationBarItem(icon: new Icon(Icons.home), title: new Text("Home")),
                  new BottomNavigationBarItem(icon: new Icon(Icons.ac_unit), title: new Text("AC Unit"))
                ]),
              ),
              body: new SafeArea(
                child: new Container(
                  constraints: new BoxConstraints.expand(),
                  color: Colors.green,
                  child: new Column(
                    children: <Widget>[
                      new RaisedButton(
                          child: new Text("Press me"),
                          onPressed: () {
                            Navigator.push(
                                context,
                                new MaterialPageRoute(
                                    builder: (context) => new Scaffold(
                                          bottomNavigationBar: new Hero(
                                            tag: "bottomNavigationBar",
                                            child: new BottomNavigationBar(items: [
                                              new BottomNavigationBarItem(icon: new Icon(Icons.home), title: new Text("Home")),
                                              new BottomNavigationBarItem(icon: new Icon(Icons.ac_unit), title: new Text("AC Unit"))
                                            ]),
                                          ),
                                          body: new SafeArea(
                                            child: new Container(
                                              constraints:
                                                  new BoxConstraints.expand(),
                                              color: Colors.red,
                                              child: new Column(
                                                children: <Widget>[
                                                  new RaisedButton(
                                                    onPressed: () =>
                                                        Navigator.pop(context),
                                                    child: new Text("Back"),
                                                  )
                                                ],
                                              ),
                                            ),
                                          ),
                                        )));
                          })
                    ],
                  ),
                ),
              ),
            ),
      ),
    ));

I don't know how well the hero system handles multiple heroes etc, and if you say wanted to animate the navigation bar this might not work overly well.

There is another way of doing this which would allow you to animate the bottom navigation bar; it's actually a question that has already been answered though: Flutter: Hero transition + widget animation at the same time?

Hallucinogen answered 3/4, 2018 at 16:36 Comment(0)
R
1

It's very easy when you use GoRouter (https://pub.dev/packages/go_router) as a Navigation. It has a ShellRoute option - you can wrap routes that you want to have the persistent navigation bar (or any other element like app bar and others) with this Shell. https://www.youtube.com/watch?v=b6Z885Z46cU

example:

ShellRoute(
      builder: (context, state, child) {
        return BottomNavigationPage(
          child: child,
        );
      },
      routes: [
        GoRoute(
          path: '/home',
          builder: (BuildContext context, GoRouterState state) {
            return const HomePage();
          },
        ),
        GoRoute(
          path: '/settings',
          builder: (BuildContext context, GoRouterState state) {
            return const SettingsPage();
          },
        ),
      ],
    ),
Radiate answered 29/6, 2023 at 21:36 Comment(0)
A
0

The NavigationBar API docs have an example (albeit complicated, IMO) that shows how to do this.

Apiarian answered 2/2 at 17:50 Comment(0)
D
0

This solution works for me, and only load widget when clicked from bottom bar:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:tracker/views/screens/Settings.dart';

import 'HomePage.dart';
import 'Status.dart';
import 'Users.dart';

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _selectedIndex = 0;

  Map<int, GlobalKey<NavigatorState>> navigatorKeys = {
    0: GlobalKey<NavigatorState>(),
    1: GlobalKey<NavigatorState>(),
    2: GlobalKey<NavigatorState>(),
    3: GlobalKey<NavigatorState>(),
  };
  final List<Widget> _pages = [
    const HomePage(),
    const Users(),
    const Status(),
    Settings(),
  ];

  void _onItemTapped(int index) {
    if (index == _selectedIndex) {
      // If tapped index is already selected, pop all routes until reaching the root
      navigatorKeys[_selectedIndex]!.currentState!.popUntil((route) => route.isFirst);
    } else {
      setState(() {
        _selectedIndex = index;
      });
    }
  }


  buildNavigator() {
    return Navigator(
      key: navigatorKeys[_selectedIndex],
      onGenerateRoute: (RouteSettings settings) {
        return MaterialPageRoute(
            builder: (_) => _pages.elementAt(_selectedIndex));
      },
    );
  }

  bool _shouldExit = false;

  Future<bool> _onWillPop() async {
    final isFirstRouteInCurrentTab =
        !await navigatorKeys[_selectedIndex]!.currentState!.maybePop();
    if (isFirstRouteInCurrentTab) {
      if (_shouldExit) {
        // If already pressed once, exit the app
        return true;
      } else {
        // Set _shouldExit to true and notify the user to press again to exit
        _shouldExit = true;
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Press again to exit")),
        );
        // Reset _shouldExit after a delay
        Future.delayed(const Duration(seconds: 2), () {
          _shouldExit = false;
        });
        return false;
      }
    }

    return isFirstRouteInCurrentTab;
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPop,
      child: Scaffold(
        body: buildNavigator(),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: _selectedIndex,
          onTap: _onItemTapped,
          backgroundColor: Colors.transparent,
          // Set background color to transparent
          elevation: 0,
          // Remove shadow
          type: BottomNavigationBarType.fixed,

          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Image.asset(
                'assets/1.png',
                width: 80,
                height: 80,
              ),
              label: '',
            ),
            BottomNavigationBarItem(
              icon: Image.asset(
                'assets/2.png',
                width: 80,
                height: 80,
              ),
              label: '',
            ),
            BottomNavigationBarItem(
              icon: Image.asset(
                'assets/3.png',
                width: 80,
                height: 80,
              ),
              label: '',
            ),
            BottomNavigationBarItem(
              icon: Image.asset(
                'assets/4.png',
                width: 80,
                height: 80,
              ),
              label: '',
            ),
          ],
        ),
      ),
    );
  }
}
Doyenne answered 17/2 at 22:32 Comment(1)
Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?Knudson
A
0

You can use persistent_bottom_nav_bar package.

Example:

PersistentNavBarNavigator.pushNewScreen(
    context,
    screen: NewScreen(),
    withNavBar: true, // true = show BottomNavigationBar
    pageTransitionAnimation: PageTransitionAnimation.cupertino,
);
Adumbral answered 21/2 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.