How to Build Bottom Navigation Bar Using GetX in Flutter

Learn how to build a reactive and high-performance Bottom Navigation Bar in Flutter using GetX. Step-by-step tutorial with complete code source, state preservation using IndexedStack, and architectural best practices

Published July 04, 2026

Implementing a clean, reliable persistent menu is essential for providing a smooth mobile experience. In standard Flutter development, managing state across multiple tabs using a traditional StatefulWidget can quickly result in tightly coupled, messy code.

Using GetX—a powerful, lightweight state management solution—decouples business logic from presentation layers entirely. This tutorial guides you through the step-by-step process of building an optimized, reactive Bottom Navigation Bar using GetX.

Why Choose GetX for Bottom Navigation?

  • Decoupled Architecture: Business logic is entirely separated from user interface elements.

  • Lightweight & High-Performance: Avoids redundant state rebuilds by updating only specified reactive components.

  • No BuildContext Required: Navigation and state transitions occur independently of the widget tree context.

Step 1: Add Necessary Dependencies

Open your project's pubspec.yaml file and declare the get package dependency under the dependencies block:

YAML file

dependencies:
  flutter:
    sdk: flutter
  get: ^4.6.6 # Use the latest stable version

 

 

Run the following command in your terminal to pull the package into your workspace:

flutter pub get

 

Step 2: Establish the GetX Controller

The BottomNavController extends GetxController to monitor the currently selected tab index reactively via an observable value (.obs).

Create a file named bottom_nav_controller.dart:

 

import 'package:get/get.dart';

class BottomNavController extends GetxController {
  // Reactive variable tracking the active tab index
  var selectedIndex = 0.obs;

  // Modifies the active index value when a user taps an item
  void changeIndex(int index) {
    selectedIndex.value = index;
  }
}

 

Step 3: Define Individual Target Screens

Prepare distinctive placeholders for each page corresponding to a navigation bar item.

Create navigation_screens.dart:

 

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : Size(double.infinity, double.infinity);

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('Home Screen', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('Search Screen', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('Profile Screen', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    );
  }
}

 

 

Step 4: Construct the Core Dashboard Scaffold

Inject your controller and wrap the BottomNavigationBar in an Obx widget to listen dynamically for tab selection updates.

Create dashboard_screen.dart:

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'bottom_nav_controller.dart';
import 'navigation_screens.dart';

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

  // Initialize the controller instance
  final BottomNavController controller = Get.put(BottomNavController());

  // Define screen display sequence mapped to indices
  final List<Widget> screens = [
    const HomeScreen(),
    const SearchScreen(),
    const ProfileScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GetX Bottom Navigation'),
        centerTitle: true,
        backgroundColor: Colors.blueAccent,
      ),
      // Obx dynamically switches views based on selectedIndex modifications
      body: Obx(() => IndexedStack(
            index: controller.selectedIndex.value,
            children: screens,
          )),
      bottomNavigationBar: Obx(
        () => BottomNavigationBar(
          currentIndex: controller.selectedIndex.value,
          onTap: controller.changeIndex,
          selectedItemColor: Colors.blueAccent,
          unselectedItemColor: Colors.grey,
          showUnselectedLabels: false,
          items: const [
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.search),
              label: 'Search',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.person),
              label: 'Profile',
            ),
          ],
        ),
      ),
    );
  }
}

 

Design Tip: Using IndexedStack inside the body ensures that state is preserved across individual target screens when navigating back and forth, preventing constant widget initialization.

 

Step 5: Configure the Application Entry Point

To enable clean dependency management and route handling via GetX, initialize your application using a GetMaterialApp root wrapper.

Modify main.dart:

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dashboard_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Bottom Navigation Bar',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DashboardScreen(),
    );
  }
}

 

Conclusion

You have successfully constructed a scalable, state-managed Bottom Navigation Bar utilizing GetX. The presentation layers are lightweight, and view changes are tracked reactively without forcing unnecessary layout rebuilds. This foundation can be extended by embedding custom navigation structures or deep nested actions inside each index container seamlessly.