image

04 Dec 2025

9K

35K

Creating Dynamic TabBars in Flutter

TabBars are a fundamental UI component in many mobile applications, allowing users to navigate between different sections of content within the same screen. While static TabBars are straightforward to implement, scenarios often arise where the tabs need to be dynamic – meaning their number, titles, or even content change based on external data, user roles, or application state. This article will guide you through creating dynamic TabBars in Flutter, providing a robust and flexible solution.

Understanding the Basics: Static TabBar

Before diving into dynamism, let's briefly recall how a static TabBar is typically constructed. It involves a DefaultTabController, a TabBar, and a TabBarView.


import 'package:flutter/material.dart';

class StaticTabBarExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3, // Number of tabs
      child: Scaffold(
        appBar: AppBar(
          title: Text('Static TabBar'),
          bottom: TabBar(
            tabs: [
              Tab(icon: Icon(Icons.home), text: 'Home'),
              Tab(icon: Icon(Icons.settings), text: 'Settings'),
              Tab(icon: Icon(Icons.info), text: 'About'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            Center(child: Text('Home Content')),
            Center(child: Text('Settings Content')),
            Center(child: Text('About Content')),
          ],
        ),
      ),
    );
  }
}

In this example, the tabs and their corresponding content are hardcoded, making them static and unchanging.

The Need for Dynamism

Dynamic TabBars become essential in various real-world applications:

  • Data-driven Tabs: Tabs generated from an API response (e.g., categories in an e-commerce app).
  • User Role-based Tabs: Different tabs visible based on the logged-in user's permissions.
  • Configuration-driven Tabs: Tabs defined in a configuration file or remote backend.
  • Dynamic Content: The content within a tab itself might change, but here we focus on the tabs themselves.

To achieve dynamism, we need a way to manage the tab data and use it to programmatically build the TabBar and TabBarView children.

Strategy for Dynamic TabBars

The core strategy involves storing the tab information in a mutable data structure, typically a List. This list will contain custom objects, each representing a single tab and holding its title, icon (optional), and the widget that serves as its content.

1. Define a TabItem Model

First, let's create a simple data model to represent each tab's properties:


import 'package:flutter/material.dart';

class TabItem {
  final String title;
  final IconData icon; // Optional
  final Widget content;

  TabItem({required this.title, this.icon = Icons.star, required this.content});
}

2. Manage Tab Data in a StatefulWidget

Since the tabs will change, we need a StatefulWidget to manage the list of TabItem objects. This list will be the source of truth for our dynamic tabs.


import 'package:flutter/material.dart';
// import 'package:your_app/models/tab_item.dart'; // Assuming tab_item.dart is in models folder

// Placeholder for TabItem if not imported
class TabItem {
  final String title;
  final IconData icon;
  final Widget content;

  TabItem({required this.title, this.icon = Icons.star, required this.content});
}

class DynamicTabBarScreen extends StatefulWidget {
  @override
  _DynamicTabBarScreenState createState() => _DynamicTabBarScreenState();
}

class _DynamicTabBarScreenState extends State {
  List<TabItem> _tabItems = [];
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _initializeTabs();
  }

  void _initializeTabs() async {
    setState(() {
      _isLoading = true;
    });
    // Simulate fetching data or setting up tabs dynamically
    // In a real app, this might come from an API call, database, etc.
    await Future.delayed(Duration(seconds: 1)); // Simulate network delay

    setState(() {
      _tabItems = [
        TabItem(title: 'Products', icon: Icons.shopping_bag, content: Center(child: Text('All Products'))),
        TabItem(title: 'Categories', icon: Icons.category, content: Center(child: Text('Product Categories'))),
        TabItem(title: 'Favorites', icon: Icons.favorite, content: Center(child: Text('Your Favorite Items'))),
        TabItem(title: 'About', icon: Icons.info, content: Center(child: Text('About This App'))),
      ];
      _isLoading = false;
    });
  }

  // ... build method will go here
}

In the _initializeTabs method, we simulate an asynchronous operation to fetch tab data. Once the data is ready, setState is called to rebuild the UI with the new tabs.

3. Building the TabBar and TabBarView

Now, we use the _tabItems list to dynamically generate the tabs for the TabBar and children for the TabBarView. The crucial part is to ensure that the length property of DefaultTabController matches the number of items in _tabItems.


  // ... inside _DynamicTabBarScreenState class ...

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Scaffold(
        appBar: AppBar(title: Text('Dynamic TabBar')),
        body: Center(child: CircularProgressIndicator()), // Show loading indicator
      );
    }

    if (_tabItems.isEmpty) {
      return Scaffold(
        appBar: AppBar(title: Text('Dynamic TabBar')),
        body: Center(child: Text('No tabs available.')), // Handle empty state
      );
    }

    return DefaultTabController(
      length: _tabItems.length, // Dynamic length based on our data
      child: Scaffold(
        appBar: AppBar(
          title: Text('Dynamic TabBar'),
          bottom: TabBar(
            tabs: _tabItems.map((item) => Tab(icon: Icon(item.icon), text: item.title)).toList(),
            isScrollable: true, // Useful if you have many tabs
            // Customize TabBar further (optional)
            indicatorColor: Colors.white,
            labelColor: Colors.white,
            unselectedLabelColor: Colors.white70,
          ),
        ),
        body: TabBarView(
          children: _tabItems.map((item) => item.content).toList(),
        ),
      ),
    );
  }

Notice the use of .map().toList() to transform our _tabItems list into a list of Tab widgets and a list of content Widgets respectively. The isScrollable: true property on TabBar is helpful if you anticipate having many tabs that might not fit on the screen.

Complete Example

Here's a full runnable example combining all the pieces:


import 'package:flutter/material.dart';

// 1. Define TabItem Model
class TabItem {
  final String title;
  final IconData icon;
  final Widget content;

  TabItem({required this.title, this.icon = Icons.star, required this.content});
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dynamic TabBar Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DynamicTabBarScreen(),
    );
  }
}

// 2. Dynamic TabBar Screen (StatefulWidget)
class DynamicTabBarScreen extends StatefulWidget {
  @override
  _DynamicTabBarScreenState createState() => _DynamicTabBarScreenState();
}

class _DynamicTabBarScreenState extends State {
  List<TabItem> _tabItems = [];
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _initializeTabs();
  }

  void _initializeTabs() async {
    setState(() {
      _isLoading = true;
    });

    // Simulate fetching data from an API or configuration
    await Future.delayed(Duration(seconds: 2));

    List<TabItem> fetchedItems = [
      TabItem(title: 'Dashboard', icon: Icons.dashboard, content: Center(child: Text('Welcome to Dashboard'))),
      TabItem(title: 'Analytics', icon: Icons.analytics, content: Center(child: Text('View Analytics Data'))),
      TabItem(title: 'Reports', icon: Icons.receipt, content: Center(child: Text('Generate Reports'))),
      TabItem(title: 'Users', icon: Icons.group, content: Center(child: Text('Manage Users'))),
    ];

    // Example of conditional tabs based on some logic (e.g., user role)
    // bool userIsAdmin = true;
    // if (userIsAdmin) {
    //   fetchedItems.add(TabItem(title: 'Admin', icon: Icons.admin_panel_settings, content: Center(child: Text('Admin Panel'))));
    // }

    setState(() {
      _tabItems = fetchedItems;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Scaffold(
        appBar: AppBar(title: Text('Dynamic TabBar')),
        body: Center(child: CircularProgressIndicator()),
      );
    }

    if (_tabItems.isEmpty) {
      return Scaffold(
        appBar: AppBar(title: Text('Dynamic TabBar')),
        body: Center(child: Text('No tabs available.')),
      );
    }

    return DefaultTabController(
      length: _tabItems.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Dynamic TabBar'),
          bottom: TabBar(
            tabs: _tabItems.map((item) => Tab(icon: Icon(item.icon), text: item.title)).toList(),
            isScrollable: true,
            // Customize TabBar further (optional)
            indicatorColor: Colors.white,
            labelColor: Colors.white,
            unselectedLabelColor: Colors.white70,
          ),
        ),
        body: TabBarView(
          children: _tabItems.map((item) => item.content).toList(),
        ),
      ),
    );
  }
}

Considerations and Best Practices

  • State Management: For more complex applications, consider using a dedicated state management solution like Provider, BLoC/Cubit, Riverpod, or GetX to manage the _tabItems list. This separates concerns and makes your code more scalable and testable.
  • Error Handling: Implement robust error handling for fetching tab data (e.g., network errors, empty responses). Display appropriate feedback to the user.
  • Loading States: Always show a loading indicator while tab data is being fetched to improve user experience. Handle cases where no tabs are available after fetching.
  • Performance of TabBarView: TabBarView by default builds all its children when the widget is created. If you have many tabs with complex content, this can impact performance. For very complex scenarios, consider using a package that supports lazy loading or custom solutions, or ensure your tab content widgets are optimized.
  • Initial Tab Selection: If you need to programmatically select a specific tab on load (e.g., based on a deep link), you can use a TabController explicitly instead of DefaultTabController.
  • Responsiveness: Ensure your tab titles and content are responsive across different screen sizes. Use isScrollable: true for many tabs.

Conclusion

Creating dynamic TabBars in Flutter is a powerful technique for building flexible and data-driven user interfaces. By defining a simple data model, managing a list of these items in a StatefulWidget (or a state management solution), and leveraging the .map() function, you can effortlessly generate TabBars and TabBarViews that adapt to your application's evolving needs. This approach provides a clean and maintainable way to handle dynamic navigation within your Flutter applications.

Related Articles

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera

Dec 19, 2025

Building a Countdown Timer Widget in Flutter

Building a Countdown Timer Widget in Flutter Countdown timers are a fundamental component in many modern applications, ranging from e-commerce platforms indica