What is IndexedStack and what is the use of it along with BottomNavigationBar?
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
dartIndexedStack( 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
indexWhy 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
dartimport '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)
dartclass 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)
dartclass 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
| Approach | State Preserved | Memory Usage | Build Cost | Use Case |
|---|---|---|---|---|
| IndexedStack | ✅ Yes | High (all screens in memory) | Low (no rebuild) | BottomNav, Tabs |
| PageView | ✅ Yes (with text | High | Medium | Swipeable pages |
| Navigator | ❌ No (unless using routes) | Low | High (rebuild on pop) | Deep navigation |
| Conditional (if/else) | ❌ No | Low | High (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
dartclass _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
dartIndexedStack( 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
dartIndexedStack( 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
dartBottomNavigationBar( 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
dartWillPopScope( 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)