flutter-bottom-navigation-bar-estructura

Bottom Navigation in Flutter with NavigationBar

  • 4 min

The main structure of an app is the organization of its base screens.

We already know how to push screens (push) and go back (pop). But modern applications are not just an infinite stack of screens.

Many have a fixed structure with a bottom or side menu that allows switching between their main sections (Home, Search, or Profile).

Today we are going to build that structure using the BottomNavigationBar.

The logic of the menu

Unlike Navigator.push, here we are not stacking a new route. We change the content of the body of the Scaffold according to the menu button we have tapped.

Therefore, we need:

  1. A StatefulWidget (because the index of the selected tab changes).
  2. An int variable to know which tab is active (0, 1, 2…).
  3. A list of Widgets (the screens we want to show).

Step-by-step implementation

Let’s create a MainScreen that will be the container of our App.

Prepare the state

We define the selected index and the list of screens.

class MainScreen extends StatefulWidget {
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _selectedIndex = 0; // Starts on the first tab

  // List of screens we want to show
  final List<Widget> _screens = [
    HomeScreen(),
    SearchScreen(),
    ProfileScreen(),
  ];

  // Function to change the index when tapping the menu
  void _onTabTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
Copied!

Add Scaffold and BottomNavigationBar

Now we assemble the visual structure.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // In the body we show the screen corresponding to the index
      body: _screens[_selectedIndex], 
      
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex, // Tells it which one is active
        onTap: _onTabTapped,          // What to do when tapped
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }
}
Copied!

It works! If you tap the icons, the body changes. But… we have a serious problem.

Preserving the state of the tabs

As we did it above (body: _screens[_selectedIndex]), every time you switch tabs, Flutter destroys the previous screen and creates the new one from scratch.

This means:

  • If you scrolled in “Home”, when you come back you will be at the top again.
  • If you typed something in a form in “Search”, when you come back it will be cleared.

To avoid this and keep the screens alive in memory, we can use the IndexedStack widget.

Using IndexedStack

This widget works like a Stack: it stacks all the screens on top of each other, but only shows the one that matches the index. The others are hidden, but alive.

@override
Widget build(BuildContext context) {
  return Scaffold(
    // ⚠️ IMPORTANT CHANGE:
    body: IndexedStack(
      index: _selectedIndex,
      children: _screens, // We pass ALL the screens
    ),
    
    bottomNavigationBar: BottomNavigationBar(...)
  );
}
Copied!

Now, when switching tabs, the state is preserved. The cost is that all screens remain mounted, so in a large application a more specific navigation strategy might be advisable.

Flutter evolves fast. Although BottomNavigationBar is the classic one, the new Material 3 standard is simply called NavigationBar.

It is more modern, the buttons are slightly taller and have a “pill” animation when selected.

bottomNavigationBar: NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: _onTabTapped,
  destinations: [
    NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
    NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
    NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
  ],
),
Copied!

The logic is identical, only the visual design changes.

Side menu with Drawer

If you prefer a hamburger menu that slides out from the side, the Scaffold has the drawer property.

The logic is a bit different: the Drawer is usually used for secondary options (Settings or Help), not for primary navigation, since it is hidden and requires two actions (open and select).

Scaffold(
  appBar: AppBar(title: Text("My App")),
  drawer: Drawer(
    child: ListView(
      children: [
        DrawerHeader(child: Text("Menu")),
        ListTile(
          leading: Icon(Icons.settings),
          title: Text("Settings"),
          onTap: () {
            // Close the drawer first
            Navigator.pop(context); 
            // Navigate
            Navigator.push(...); 
          },
        )
      ],
    ),
  ),
  body: ...
)
Copied!