47

The Flutter Gallery example of BottomNavigationBar uses a Stack of FadeTransitions in the body of the Scaffold.

I feel it would be cleaner (and easier to animate) if we could switch pages by using a Navigator.

Are there any examples of this?

Paul
  • 1,943
  • 5
  • 18
  • 18

7 Answers7

59
int index = 0;

@override
Widget build(BuildContext context) {
  return new Scaffold(
    body: new Stack(
      children: <Widget>[
        new Offstage(
          offstage: index != 0,
          child: new TickerMode(
            enabled: index == 0,
            child: new MaterialApp(home: new YourLeftPage()),
          ),
        ),
        new Offstage(
          offstage: index != 1,
          child: new TickerMode(
            enabled: index == 1,
            child: new MaterialApp(home: new YourRightPage()),
          ),
        ),
      ],
    ),
    bottomNavigationBar: new BottomNavigationBar(
      currentIndex: index,
      onTap: (int index) { setState((){ this.index = index; }); },
      items: <BottomNavigationBarItem>[
        new BottomNavigationBarItem(
          icon: new Icon(Icons.home),
          title: new Text("Left"),
        ),
        new BottomNavigationBarItem(
          icon: new Icon(Icons.search),
          title: new Text("Right"),
        ),
      ],
    ),
  );
}

You should keep each page by Stack to keep their state. Offstage stops painting, TickerMode stops animation. MaterialApp includes Navigator.

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
najeira
  • 3,133
  • 2
  • 19
  • 21
  • 1
    can you please elaborate on the purpose of the TickerMode widgets? This code seems to work fine also: new Offstage( offstage: _tabIndex != 1, child: new YourRightPage(), ), – CodeGrue Mar 30 '18 at 16:00
  • With this implementation, the back button doesn't work for accessing the previous Offstage widget. Is there any way to support this? – RafaelHamasaki Apr 02 '18 at 02:56
  • @RafaelHamasaki You can handle back button using https://docs.flutter.io/flutter/widgets/WidgetsBindingObserver/didPopRoute.html – najeira Apr 02 '18 at 03:43
  • 1
    @CodeGrue I use TickerMode to stop the animation of the widgets which are not displayed. – najeira Apr 02 '18 at 03:46
  • @najeira Yeah, that's what I ended up doing. Too bad I have to keep my own "navigation" history. Thanks anyway! – RafaelHamasaki Apr 02 '18 at 16:28
  • @najeira it seems that the code above only support 2 BottomNavigationBarItem, if I add one more item, it give an error in the simulator: RangeError(index) invalid value:Not in range 0..1, do you know why? – camino Apr 05 '18 at 01:41
  • @camino You should to add both BottomNavigationBarItem and a child of Stack. – najeira Apr 06 '18 at 02:25
  • You can have multiple nested MaterialApp widgets? (assuming this example is itself inside a parent MaterialApp) – CodeGrue May 11 '18 at 20:45
  • @CodeGrue Yes, you can. – najeira May 14 '18 at 05:57
  • I'm using this method with flutter_bloc it's GREAT, But when the app begins it loads the whole stacked-widgets data from the API, **How can I make it load only the viewed stack widget when the user view it for the first time?** – Wail Hayaly Mar 15 '20 at 10:10
  • 2
    An alternative may be [IndexedStack](https://api.flutter.dev/flutter/widgets/IndexedStack-class.html), which provides a simpler API to `Stack` combined with `Offstage`. You may still need `TickerMode` though! – publicJorn Apr 11 '20 at 08:37
  • @publicJorn Are IndexedStack and Stack+Offstage equivalent in terms of performance? – Nagabhushan Baddi Jan 15 '21 at 08:06
  • nice.. it helps me a lot – G H Prakash Oct 07 '21 at 12:44
21

Output:

enter image description here

Code:

int _index = 0;

@override
Widget build(BuildContext context) {
  Widget child;
  switch (_index) {
    case 0:
      child = FlutterLogo();
      break;
    case 1:
      child = FlutterLogo(colors: Colors.orange);
      break;
    case 2:
      child = FlutterLogo(colors: Colors.red);
      break;
  }

  return Scaffold(
    body: SizedBox.expand(child: child),
    bottomNavigationBar: BottomNavigationBar(
      onTap: (newIndex) => setState(() => _index = newIndex),
      currentIndex: _index,
      items: [
        BottomNavigationBarItem(icon: Icon(Icons.looks_one), title: Text("Blue")),
        BottomNavigationBarItem(icon: Icon(Icons.looks_two), title: Text("Orange")),
        BottomNavigationBarItem(icon: Icon(Icons.looks_3), title: Text("Red")),
      ],
    ),
  );
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 7
    Unfortunately, as I understand, your solution works only if the Scaffold body is the same widget but with different properties, which is not a regular case. – ych Jan 09 '20 at 13:03
  • 2
    this helped me a lot with simple implementation. thanks a lot – Gulnaz Ghanchi Jun 08 '20 at 09:51
  • 1
    This is simple and great it would be great if you can help me with handing backpressed in this – Amanpreet Kaur Sep 26 '20 at 07:14
  • 1
    @AmanpreetKaur For that you can use `Set`. On press of a new item, simply add its index to the set, wrap your widget in `WillPopScope` and inside `onWillPop` you can check the value of last index, if it's empty, simply exit the app else set the current index to that value. – CopsOnRoad Sep 26 '20 at 07:24
  • great answer, I used ideas here to create a container page that I just filled the child with whatever page I grabbed from the switch statement, – Mitchnoff Feb 27 '23 at 18:50
14

Here is an example how you can use Navigator with BottomNavigationBar to navigate different screen.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

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

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // This navigator state will be used to navigate different pages
  final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
  int _currentTabIndex = 0;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: Navigator(key: _navigatorKey, onGenerateRoute: generateRoute),
        bottomNavigationBar: _bottomNavigationBar(),
      ),
    );
  }

  Widget _bottomNavigationBar() {
    return BottomNavigationBar(
      type: BottomNavigationBarType.fixed,
      items: [
        BottomNavigationBarItem(
          icon: Icon(Icons.home),
          title: Text("Home"),
        ),
        BottomNavigationBarItem(
            icon: Icon(Icons.account_circle), title: Text("Account")),
        BottomNavigationBarItem(
          icon: Icon(Icons.settings),
          title: Text("Settings"),
        )
      ],
      onTap: _onTap,
      currentIndex: _currentTabIndex,
    );
  }

  _onTap(int tabIndex) {
    switch (tabIndex) {
      case 0:
        _navigatorKey.currentState.pushReplacementNamed("Home");
        break;
      case 1:
        _navigatorKey.currentState.pushReplacementNamed("Account");
        break;
      case 2:
        _navigatorKey.currentState.pushReplacementNamed("Settings");
        break;
    }
    setState(() {
      _currentTabIndex = tabIndex;
    });
  }

  Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case "Account":
        return MaterialPageRoute(builder: (context) => Container(color: Colors.blue,child: Center(child: Text("Account"))));
      case "Settings":
        return MaterialPageRoute(builder: (context) => Container(color: Colors.green,child: Center(child: Text("Settings"))));
      default:
        return MaterialPageRoute(builder: (context) => Container(color: Colors.white,child: Center(child: Text("Home"))));
    }
  }
}
Zakir
  • 1,305
  • 1
  • 10
  • 13
  • could you please elaborate more on this line: `body: Navigator(key: _navigatorKey, onGenerateRoute: generateRoute),` is it ok to set a Navigator widget as a body of your app? Looks strange. How does it work? – Kirill Karmazin Nov 19 '19 at 20:24
  • 1
    There are one or two highly complicated tutorials on this topic, containing flaws which render them basically useless. This short snippet was the only one that works. The catch is that you need to pass the pushNamed to the navigator key. Thank you, great work. – ThemBones Dec 22 '19 at 23:55
  • @KirillKarmazin If you want to navigate between different widgets. You have to put them under a navigator in widget tree. Since in this example we only want to change the body of the screen during navigation. So I add the navigator as the top widget in body and put other widget under it. – Zakir Dec 24 '19 at 16:33
  • Very well explained. only extra I did to use ChangeNotifier Provider for selected tab index. This way even if I route to page(bottom bar pages) from within the page, it activate the selected index. – Shakeel Ahmad Jun 27 '20 at 22:50
  • 1
    PushReplacementNamed is Going to purge the last screen in the stack. You need all the sections in your navigation to exist simultaneously with their own navigators intact. The only way I’ve found to do this is with an IndexedStack but I don’t use a Navigator. Still looking for a good solution. – Lee Probert Aug 03 '20 at 17:36
3

Here is example:

  int _currentIndex = 0;


  Route<Null> _getRoute(RouteSettings settings) {
    final initialSettings = new RouteSettings(
        name: settings.name,
        isInitialRoute: true);

    return new MaterialPageRoute<Null>(
        settings: initialSettings,
        builder: (context) =>
        new Scaffold(
          body: new Center(
              child: new Container(
                  height: 200.0,
                  width: 200.0,
                  child: new Column(children: <Widget>[
                    new Text(settings.name),
                    new FlatButton(onPressed: () =>
                        Navigator.of(context).pushNamed(
                            "${settings.name}/next"), child: new Text("push")),
                  ],
                  ))
          ),
          bottomNavigationBar: new BottomNavigationBar(
              currentIndex: _currentIndex,
              onTap: (value) {
                final routes = ["/list", "/map"];
                _currentIndex = value;
                Navigator.of(context).pushNamedAndRemoveUntil(
                    routes[value], (route) => false);
              },
              items: [
                new BottomNavigationBarItem(
                    icon: new Icon(Icons.list), title: new Text("List")),
                new BottomNavigationBarItem(
                    icon: new Icon(Icons.map), title: new Text("Map")),
              ]),
        ));
  }

  @override
  Widget build(BuildContext context) =>
      new MaterialApp(
        initialRoute: "/list",
        onGenerateRoute: _getRoute,
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
      );

You can set isInitialRoute to true and pass it to MaterialPageRoute. It will remove pop animation.

And to remove old routes you can use pushNamedAndRemoveUntil

Navigator.of(context).pushNamedAndRemoveUntil(routes[value], (route) => false);

To set current page you can have a variable in your state _currentIndex and assign it to BottomNavigationBar:

German Saprykin
  • 6,631
  • 2
  • 29
  • 26
1

Glad You asked, I experimented with this a couple of months back and tried to simplify this through a blog post. I won't be able to post the complete code here since it is pretty long, But I can certainly link all the resources to clarify it.

  1. Everything about the BottomNavigationBar in flutter
  2. complete sample code
  3. Dartpad demo
  4. If you prefer you can also depend on this package https://pub.dev/packages/navbar_router

Here's the resulting output of what the article helps you build

enter image description here

Mahesh Jamdade
  • 17,235
  • 8
  • 110
  • 131
0
Navigator.of(context).pushNamedAndRemoveUntil(
                routes[value], (route) => true);

I had to use true to enable back button.

NB: I was using Navigator.pushNamed() for navigation.

Kit Mateyawa
  • 197
  • 2
  • 15
-1

This is the code I am using in my project. If you try to avoid page viewer so you can try this

import 'package:flutter/material.dart';

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

  @override
  State<Dashboard> createState() => _DashboardState();
}

class _DashboardState extends State<Dashboard> {
  int _selectedIndex = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sample'),
      ),
      body: SingleChildScrollView(
       child: Column(
        children: [
          if (_selectedIndex == 0)
            // you can call custom widget here
            Column(
              children: const [
                Text("0"),
              ],
            )
          else if (_selectedIndex == 1)
             Column(
                children: const [
                  Text("1"),
                ],                 
            )
          else             
               Column(
                children: const [
                  Text("2"),
                ],               
            ),
        ],
      ),
     ),

      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.headphones),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Business',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.school),
            label: 'School',
          ),              
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        unselectedItemColor: Colors.grey,
        onTap: _onItemTapped,
      ),
    );
  }
}

Happy Coding

Mukta
  • 1,357
  • 1
  • 15
  • 17