Question #427MediumWidgets & UIFlutter Basics

What is IndexedStack and what is the use of it along with BottomNavigationBar?

#indexed-stack#bottom-navigation-bar#navigation#widgets#state-management

Answer

Overview

IndexedStack is a Stack widget that shows only one child at a time based on an index. It's perfect for use with BottomNavigationBar because it keeps all screens alive in memory (preserving their state) while only displaying the selected one.


IndexedStack Widget

What It Does

dart
IndexedStack(
  index: selectedIndex, // Shows only this child
  children: [
    ScreenOne(),   // index: 0
    ScreenTwo(),   // index: 1
    ScreenThree(), // index: 2
  ],
)

Key Feature: All children are kept in the widget tree but only the child at

text
index
is visible. Others are hidden but state is preserved.


Why Use with BottomNavigationBar?

Problem Without IndexedStack

When switching between screens using Navigator or PageView:

  • ❌ State is lost when you navigate away
  • ❌ Scroll position resets
  • ❌ Form data disappears
  • ❌ API calls might re-fetch data

Solution With IndexedStack

  • ✅ All screens stay in memory
  • ✅ State is preserved when switching tabs
  • ✅ Scroll positions remembered
  • ✅ Form data retained
  • ✅ No unnecessary rebuilds

Complete Example

Basic Implementation

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

class HomeScreen extends StatefulWidget {
  
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _selectedIndex = 0;

  // All screens
  final List<Widget> _screens = [
    HomeTab(),
    SearchTab(),
    ProfileTab(),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: _screens,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }
}

Screen Examples

Home Tab (with scroll state)

dart
class HomeTab extends StatefulWidget {
  
  _HomeTabState createState() => _HomeTabState();
}

class _HomeTabState extends State<HomeTab> {
  final ScrollController _scrollController = ScrollController();

  
  void initState() {
    super.initState();
    print('HomeTab initialized');
  }

  
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('Item $index'),
          subtitle: Text('Scroll position is preserved!'),
        );
      },
    );
  }
}

Behavior: When you switch tabs and come back, scroll position is preserved


Search Tab (with form state)

dart
class SearchTab extends StatefulWidget {
  
  _SearchTabState createState() => _SearchTabState();
}

class _SearchTabState extends State<SearchTab> {
  final TextEditingController _controller = TextEditingController();
  List<String> _results = [];

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _search() {
    setState(() {
      _results = List.generate(
        10,
        (i) => 'Result for "${_controller.text}" #$i',
      );
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(16),
          child: TextField(
            controller: _controller,
            decoration: InputDecoration(
              hintText: 'Search...',
              suffixIcon: IconButton(
                icon: Icon(Icons.search),
                onPressed: _search,
              ),
            ),
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _results.length,
            itemBuilder: (context, index) {
              return ListTile(title: Text(_results[index]));
            },
          ),
        ),
      ],
    );
  }
}

Behavior: Text input and search results are preserved when switching tabs


IndexedStack vs Alternatives

Comparison Table

ApproachState PreservedMemory UsageBuild CostUse Case
IndexedStack✅ YesHigh (all screens in memory)Low (no rebuild)BottomNav, Tabs
PageView✅ Yes (with
text
keepAlive
)
HighMediumSwipeable pages
Navigator❌ No (unless using routes)LowHigh (rebuild on pop)Deep navigation
Conditional (if/else)❌ NoLowHigh (rebuild each time)Simple toggles

When to Use IndexedStack

✅ Good Use Cases

1. BottomNavigationBar

dart
// Perfect match - preserves state across tabs
IndexedStack(
  index: currentTabIndex,
  children: [HomeTab(), SearchTab(), ProfileTab()],
)

2. TabBar (Material tabs)

dart
// When you want to preserve scroll/form state
IndexedStack(
  index: currentTabIndex,
  children: tabs,
)

3. Settings with preview

dart
// Show different preview based on selection
IndexedStack(
  index: selectedThemeIndex,
  children: [LightPreview(), DarkPreview(), CustomPreview()],
)

❌ When NOT to Use

1. Many Screens (>10)

dart
// ❌ Bad - Uses too much memory
IndexedStack(
  index: currentIndex,
  children: List.generate(50, (i) => Screen(i)), // Too many!
)

// ✅ Better - Use PageView or Navigator
PageView(
  controller: controller,
  children: screens,
)

2. Complex/Heavy Screens

dart
// ❌ Bad - All screens loaded (video players, maps, etc.)
IndexedStack(
  children: [
    VideoPlayerScreen(),  // Heavy!
    MapScreen(),          // Heavy!
    CameraScreen(),       // Heavy!
  ],
)

// ✅ Better - Load on demand
if (currentIndex == 0) VideoPlayerScreen()
else if (currentIndex == 1) MapScreen()
else CameraScreen()

3. Deep Navigation

dart
// ❌ Bad - IndexedStack is flat, not hierarchical
IndexedStack(
  children: [
    ProductList(),
    ProductDetail(), // Can't navigate back naturally
    Cart(),
  ],
)

// ✅ Better - Use Navigator for hierarchical navigation
Navigator.push(context, MaterialPageRoute(builder: (_) => ProductDetail()));

Advanced Features

1. Lazy Loading Screens

dart
class _HomeScreenState extends State<HomeScreen> {
  int _selectedIndex = 0;

  // Initialize screens only when first accessed
  final Map<int, Widget> _screenCache = {};

  Widget _getScreen(int index) {
    if (!_screenCache.containsKey(index)) {
      switch (index) {
        case 0:
          _screenCache[index] = HomeTab();
          break;
        case 1:
          _screenCache[index] = SearchTab();
          break;
        case 2:
          _screenCache[index] = ProfileTab();
          break;
      }
    }
    return _screenCache[index]!;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: List.generate(3, (i) => _getScreen(i)),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) => setState(() => _selectedIndex = index),
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

2. Animated Transitions

dart
IndexedStack(
  index: _selectedIndex,
  sizing: StackFit.expand, // All children same size
  children: _screens.map((screen) {
    return AnimatedSwitcher(
      duration: Duration(milliseconds: 300),
      child: screen,
    );
  }).toList(),
)

3. Conditional Screen Building

dart
IndexedStack(
  index: _selectedIndex,
  children: [
    HomeTab(),
    if (user.isPremium) PremiumTab() else UpgradeTab(),
    ProfileTab(),
  ],
)

Performance Considerations

Memory Usage

dart
// Each screen in IndexedStack stays in memory
// Measure with DevTools Memory Profiler

// Example memory footprint:
// - HomeTab (ListView): ~5-10 MB
// - SearchTab (Form): ~2-3 MB
// - ProfileTab (Static): ~1-2 MB
// Total: ~8-15 MB for 3 tabs ✅ Acceptable

// But with 10 heavy screens:
// - 10 screens × 10 MB each = ~100 MB ❌ Too much!

Build Performance

dart
// IndexedStack: Only visible child rebuilds
setState(() {
  _selectedIndex = 1; // Only SearchTab rebuilds
});

// vs Conditional rendering: Everything rebuilds
setState(() {
  _selectedIndex = 1; // Entire body rebuilds
});

Common Patterns

Pattern 1: Bottom Navigation with Badge

dart
BottomNavigationBar(
  currentIndex: _selectedIndex,
  onTap: (index) => setState(() => _selectedIndex = index),
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'Home',
    ),
    BottomNavigationBarItem(
      icon: Badge(
        label: Text('3'),
        child: Icon(Icons.message),
      ),
      label: 'Messages',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.person),
      label: 'Profile',
    ),
  ],
)

Pattern 2: WillPopScope to Exit App

dart
WillPopScope(
  onWillPop: () async {
    if (_selectedIndex != 0) {
      setState(() => _selectedIndex = 0); // Go to home tab
      return false; // Don't exit
    }
    return true; // Exit app
  },
  child: Scaffold(
    body: IndexedStack(
      index: _selectedIndex,
      children: _screens,
    ),
    bottomNavigationBar: _buildBottomNav(),
  ),
)

Key Takeaways

IndexedStack = Stack widget that shows only one child at a time (based on index)

Perfect for BottomNavigationBar because it preserves state across tab switches

Trade-off: Higher memory usage (all screens in memory) vs better UX (instant switching, state preserved)

Best for: 2-5 tabs with moderate complexity

Avoid for: Many screens (>10) or very heavy screens (video, maps, camera)


Resources