Flutter

Bottom Menus

Implement bottom navigation with BottomNavigationBar, use IndexedStack to preserve page state, and manage tabs with PageController.

1. Basic BottomNavigationBar with IndexedStack

IndexedStack is the preferred approach for bottom navigation because it keeps all pages in memory and preserves their scroll position and state between tab switches — unlike switching widgets directly.

dart
import "package:flutter/material.dart";

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _selectedIndex = 0;

  // Pages are kept alive with IndexedStack
  final List<Widget> _pages = [
    const HomeTab(),
    const SearchTab(),
    const FavoritesTab(),
    const ProfileTab(),
  ];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
        type: BottomNavigationBarType.fixed,    // Required for 4+ items
        selectedItemColor: Colors.indigo,
        unselectedItemColor: Colors.grey,
        selectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold),
        backgroundColor: Colors.white,
        elevation: 8,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_outlined),
            activeIcon: Icon(Icons.home),
            label: "Home",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search_outlined),
            activeIcon: Icon(Icons.search),
            label: "Search",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite_outline),
            activeIcon: Icon(Icons.favorite),
            label: "Favorites",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_outline),
            activeIcon: Icon(Icons.person),
            label: "Profile",
          ),
        ],
      ),
    );
  }
}
2. BottomNavigationBar with PageController

Use PageController with PageView to get swipe-between-tabs behavior. Sync the controller with the bottom bar index so both stay in sync on tap or swipe.

dart
class MainScreenWithPageView extends StatefulWidget {
  const MainScreenWithPageView({super.key});

  @override
  State<MainScreenWithPageView> createState() => _MainScreenWithPageViewState();
}

class _MainScreenWithPageViewState extends State<MainScreenWithPageView> {
  int _selectedIndex = 0;
  late PageController _pageController;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(initialPage: _selectedIndex);
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  void _onTabTapped(int index) {
    setState(() => _selectedIndex = index);
    _pageController.animateToPage(
      index,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        controller: _pageController,
        physics: const NeverScrollableScrollPhysics(), // Disable swipe
        onPageChanged: (index) {
          setState(() => _selectedIndex = index);
        },
        children: const [
          HomeTab(),
          SearchTab(),
          ProfileTab(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: _onTabTapped,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: "Search"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "Profile"),
        ],
      ),
    );
  }
}
3. Custom Styled Bottom Navigation Bar

Customize the bottom bar with a floating effect using Container and BoxDecoration with shadow, or use the NavigationBar (Material 3) widget for a modern pill-indicator style.

dart
// Material 3 NavigationBar (modern pill indicator)
Scaffold(
  bottomNavigationBar: NavigationBar(
    selectedIndex: _selectedIndex,
    onDestinationSelected: (index) {
      setState(() => _selectedIndex = index);
    },
    backgroundColor: Colors.white,
    indicatorColor: Colors.indigo.withOpacity(0.15),
    destinations: const [
      NavigationDestination(
        icon: Icon(Icons.home_outlined),
        selectedIcon: Icon(Icons.home, color: Colors.indigo),
        label: "Home",
      ),
      NavigationDestination(
        icon: Icon(Icons.search_outlined),
        selectedIcon: Icon(Icons.search, color: Colors.indigo),
        label: "Search",
      ),
      NavigationDestination(
        icon: Icon(Icons.notifications_outlined),
        selectedIcon: Icon(Icons.notifications, color: Colors.indigo),
        label: "Alerts",
      ),
      NavigationDestination(
        icon: Icon(Icons.person_outline),
        selectedIcon: Icon(Icons.person, color: Colors.indigo),
        label: "Profile",
      ),
    ],
  ),
  body: IndexedStack(
    index: _selectedIndex,
    children: _pages,
  ),
)

// Floating bottom nav with Card + shadow
Scaffold(
  extendBody: true,                          // Body extends under nav bar
  bottomNavigationBar: Padding(
    padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
    child: ClipRRect(
      borderRadius: BorderRadius.circular(24),
      child: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (i) => setState(() => _selectedIndex = i),
        elevation: 0,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
          BottomNavigationBarItem(icon: Icon(Icons.explore), label: "Explore"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "Me"),
        ],
      ),
    ),
  ),
)
4. Preserving State with AutomaticKeepAliveClientMixin

When using PageView (not IndexedStack), pages get disposed on tab switch. Add AutomaticKeepAliveClientMixin to a tab's state to prevent it from being destroyed.

dart
class HomeTab extends StatefulWidget {
  const HomeTab({super.key});

  @override
  State<HomeTab> createState() => _HomeTabState();
}

// Add AutomaticKeepAliveClientMixin to preserve state
class _HomeTabState extends State<HomeTab>
    with AutomaticKeepAliveClientMixin {

  int _counter = 0;

  @override
  bool get wantKeepAlive => true;   // Return true to keep alive

  @override
  Widget build(BuildContext context) {
    super.build(context);   // Must call super.build()

    return Scaffold(
      appBar: AppBar(title: const Text("Home")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Counter: $_counter", style: const TextStyle(fontSize: 24)),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => setState(() => _counter++),
              child: const Text("Increment"),
            ),
          ],
        ),
      ),
    );
  }
}
5. Bottom Navigation with Badge (Notification Count)

Use the Badge widget (Flutter 3.7+) or wrap an icon with a Stack to show notification counts on bottom nav items.

dart
// Using Flutter 3.7+ Badge widget
BottomNavigationBarItem(
  icon: Badge(
    label: const Text("3"),
    child: const Icon(Icons.notifications_outlined),
  ),
  label: "Alerts",
)

// Custom badge with Stack (older Flutter)
Widget _iconWithBadge(IconData icon, int count) {
  return Stack(
    clipBehavior: Clip.none,
    children: [
      Icon(icon),
      if (count > 0)
        Positioned(
          right: -6,
          top: -4,
          child: Container(
            padding: const EdgeInsets.all(3),
            decoration: const BoxDecoration(
              color: Colors.red,
              shape: BoxShape.circle,
            ),
            constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
            child: Text(
              count > 99 ? "99+" : "$count",
              style: const TextStyle(
                color: Colors.white,
                fontSize: 10,
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ),
    ],
  );
}

// Use in BottomNavigationBarItem
BottomNavigationBarItem(
  icon: _iconWithBadge(Icons.notifications_outlined, 5),
  activeIcon: _iconWithBadge(Icons.notifications, 5),
  label: "Alerts",
)