infinite_scrollable_tabbar 0.0.1 copy "infinite_scrollable_tabbar: ^0.0.1" to clipboard
infinite_scrollable_tabbar: ^0.0.1 copied to clipboard

A fully customizable Flutter tab bar with infinite scrolling capability. Create horizontally scrollable tab bars where tabs repeat seamlessly in both directions.

example/lib/main.dart

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Infinite Scrollable TabBar Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const DemoHomePage(),
    );
  }
}

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

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage> {
  int _selectedDemoIndex = 0;

  final List<Widget> _demos = const [
    BasicDemo(),
    StyledDemo(),
    IconsDemo(),
    ProgrammaticScrollDemo(),
    DynamicTabsDemo(),
  ];

  final List<String> _demoNames = const [
    'Basic',
    'Styled',
    'With Icons',
    'Programmatic',
    'Dynamic',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Infinite Scrollable TabBar Demo'),
      ),
      body: Column(
        children: [
          Container(
            padding: const EdgeInsets.all(16.0),
            color: Colors.grey[200],
            child: Wrap(
              spacing: 8.0,
              children: List.generate(_demos.length, (index) {
                return ChoiceChip(
                  label: Text(_demoNames[index]),
                  selected: _selectedDemoIndex == index,
                  onSelected: (selected) {
                    if (selected) {
                      setState(() {
                        _selectedDemoIndex = index;
                      });
                    }
                  },
                );
              }),
            ),
          ),
          Expanded(child: _demos[_selectedDemoIndex]),
        ],
      ),
    );
  }
}

/// Basic usage demo with simple text tabs
class BasicDemo extends StatefulWidget {
  const BasicDemo({super.key});

  @override
  State<BasicDemo> createState() => _BasicDemoState();
}

class _BasicDemoState extends State<BasicDemo> {
  late final InfiniteScrollableTabController _controller;
  late final List<TabItem> _tabs;

  @override
  void initState() {
    super.initState();
    _tabs = List.generate(5, (index) => TabItem(label: 'Tab ${index + 1}'));
    _controller = InfiniteScrollableTabController(
      initialIndex: 0,
      tabCount: _tabs.length,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        InfiniteScrollableTabBar(
          controller: _controller,
          tabs: _tabs,
          onTabTap: (index) {
            setState(() {});
          },
        ),
        Expanded(
          child: Center(
            child: Text(
              'Selected: ${_tabs[_controller.currentIndex].label}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ),
        ),
      ],
    );
  }
}

/// Demo with custom styling
class StyledDemo extends StatefulWidget {
  const StyledDemo({super.key});

  @override
  State<StyledDemo> createState() => _StyledDemoState();
}

class _StyledDemoState extends State<StyledDemo> {
  late final InfiniteScrollableTabController _controller;
  late final List<TabItem> _tabs;

  @override
  void initState() {
    super.initState();
    _tabs = List.generate(7, (index) => TabItem(label: 'Item ${index + 1}'));
    _controller = InfiniteScrollableTabController(
      initialIndex: 0,
      tabCount: _tabs.length,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        InfiniteScrollableTabBar(
          controller: _controller,
          tabs: _tabs,
          height: 70,
          tabWidth: 120,
          backgroundColor: Colors.deepPurple[50],
          selectedTabStyle: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
            color: Colors.deepPurple,
          ),
          unselectedTabStyle: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.normal,
            color: Colors.grey[600],
          ),
          indicatorBuilder:
              () => Container(
                width: 80,
                height: 4,
                decoration: BoxDecoration(
                  gradient: const LinearGradient(
                    colors: [Colors.deepPurple, Colors.purpleAccent],
                  ),
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
          onTabTap: (index) {
            setState(() {});
          },
        ),
        Expanded(
          child: Container(
            color: Colors.deepPurple[50],
            child: Center(
              child: Card(
                elevation: 4,
                child: Padding(
                  padding: const EdgeInsets.all(24.0),
                  child: Text(
                    'Selected: ${_tabs[_controller.currentIndex].label}',
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                      color: Colors.deepPurple,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

/// Demo with icons in tabs
class IconsDemo extends StatefulWidget {
  const IconsDemo({super.key});

  @override
  State<IconsDemo> createState() => _IconsDemoState();
}

class _IconsDemoState extends State<IconsDemo> {
  late final InfiniteScrollableTabController _controller;
  late final List<TabItem> _tabs;

  final List<IconData> _icons = [
    Icons.home,
    Icons.search,
    Icons.favorite,
    Icons.person,
    Icons.settings,
    Icons.notifications,
    Icons.chat,
  ];

  final List<String> _labels = [
    'Home',
    'Search',
    'Favorites',
    'Profile',
    'Settings',
    'Alerts',
    'Messages',
  ];

  @override
  void initState() {
    super.initState();
    _tabs = List.generate(
      _icons.length,
      (index) => TabItem(label: _labels[index], icon: Icon(_icons[index])),
    );
    _controller = InfiniteScrollableTabController(
      initialIndex: 0,
      tabCount: _tabs.length,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        InfiniteScrollableTabBar(
          controller: _controller,
          tabs: _tabs,
          height: 90,
          tabWidth: 100,
          backgroundColor: Colors.white,
          selectedTabStyle: const TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.bold,
            color: Colors.blue,
          ),
          unselectedTabStyle: const TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.normal,
            color: Colors.grey,
          ),
          indicatorPosition: IndicatorPosition.top,
          onTabTap: (index) {
            setState(() {});
          },
        ),
        const Divider(height: 1),
        Expanded(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  _icons[_controller.currentIndex],
                  size: 80,
                  color: Colors.blue,
                ),
                const SizedBox(height: 16),
                Text(
                  _labels[_controller.currentIndex],
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

/// Demo showing programmatic scrolling
class ProgrammaticScrollDemo extends StatefulWidget {
  const ProgrammaticScrollDemo({super.key});

  @override
  State<ProgrammaticScrollDemo> createState() => _ProgrammaticScrollDemoState();
}

class _ProgrammaticScrollDemoState extends State<ProgrammaticScrollDemo> {
  late final InfiniteScrollableTabController _controller;
  late final List<TabItem> _tabs;

  @override
  void initState() {
    super.initState();
    _tabs = List.generate(10, (index) => TabItem(label: 'Tab ${index + 1}'));
    _controller = InfiniteScrollableTabController(
      initialIndex: 0,
      tabCount: _tabs.length,
    );
  }

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

  void _scrollToRandom() {
    final randomIndex = DateTime.now().millisecond % _tabs.length;
    _controller.animateToIndex(randomIndex);
  }

  void _scrollNext() {
    final nextIndex = (_controller.currentIndex + 1) % _tabs.length;
    _controller.animateToIndex(nextIndex);
  }

  void _scrollPrevious() {
    final prevIndex =
        (_controller.currentIndex - 1 + _tabs.length) % _tabs.length;
    _controller.animateToIndex(prevIndex);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        InfiniteScrollableTabBar(
          controller: _controller,
          tabs: _tabs,
          onTabTap: (index) {
            setState(() {});
          },
        ),
        Expanded(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Current Index: ${_controller.currentIndex}',
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
                const SizedBox(height: 32),
                Wrap(
                  spacing: 12,
                  runSpacing: 12,
                  alignment: WrapAlignment.center,
                  children: [
                    ElevatedButton.icon(
                      onPressed: _scrollPrevious,
                      icon: const Icon(Icons.arrow_back),
                      label: const Text('Previous'),
                    ),
                    ElevatedButton.icon(
                      onPressed: _scrollNext,
                      icon: const Icon(Icons.arrow_forward),
                      label: const Text('Next'),
                    ),
                    ElevatedButton.icon(
                      onPressed: _scrollToRandom,
                      icon: const Icon(Icons.shuffle),
                      label: const Text('Random'),
                    ),
                  ],
                ),
                const SizedBox(height: 32),
                const Text(
                  'Tap the buttons above to programmatically\nscroll to different tabs',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.grey),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

/// Demo with dynamic tabs that can be added/removed
class DynamicTabsDemo extends StatefulWidget {
  const DynamicTabsDemo({super.key});

  @override
  State<DynamicTabsDemo> createState() => _DynamicTabsDemoState();
}

class _DynamicTabsDemoState extends State<DynamicTabsDemo> {
  late InfiniteScrollableTabController _controller;
  late List<TabItem> _tabs;
  int _tabCounter = 3;

  @override
  void initState() {
    super.initState();
    _tabs = [
      const TabItem(label: 'Tab 1'),
      const TabItem(label: 'Tab 2'),
      const TabItem(label: 'Tab 3'),
    ];
    _controller = InfiniteScrollableTabController(
      initialIndex: 0,
      tabCount: _tabs.length,
    );
  }

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

  void _addTab() {
    setState(() {
      _tabCounter++;
      _tabs.add(TabItem(label: 'Tab $_tabCounter'));

      // Create a new controller with updated tab count
      final currentIndex = _controller.currentIndex;
      _controller.dispose();
      _controller = InfiniteScrollableTabController(
        initialIndex: currentIndex,
        tabCount: _tabs.length,
      );
    });
  }

  void _removeTab() {
    if (_tabs.length > 1) {
      setState(() {
        _tabs.removeLast();

        // Create a new controller with updated tab count
        final currentIndex =
            _controller.currentIndex >= _tabs.length
                ? _tabs.length - 1
                : _controller.currentIndex;
        _controller.dispose();
        _controller = InfiniteScrollableTabController(
          initialIndex: currentIndex,
          tabCount: _tabs.length,
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        InfiniteScrollableTabBar(
          controller: _controller,
          tabs: _tabs,
          onTabTap: (index) {
            setState(() {});
          },
        ),
        Expanded(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Total Tabs: ${_tabs.length}',
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
                const SizedBox(height: 8),
                Text(
                  'Selected: ${_tabs[_controller.currentIndex].label}',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
                const SizedBox(height: 32),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton.icon(
                      onPressed: _addTab,
                      icon: const Icon(Icons.add),
                      label: const Text('Add Tab'),
                    ),
                    const SizedBox(width: 16),
                    ElevatedButton.icon(
                      onPressed: _tabs.length > 1 ? _removeTab : null,
                      icon: const Icon(Icons.remove),
                      label: const Text('Remove Tab'),
                    ),
                  ],
                ),
                const SizedBox(height: 32),
                const Padding(
                  padding: EdgeInsets.symmetric(horizontal: 32.0),
                  child: Text(
                    'Add or remove tabs dynamically.\nThe infinite scrolling adapts automatically!',
                    textAlign: TextAlign.center,
                    style: TextStyle(color: Colors.grey),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}
0
likes
150
points
--
downloads

Publisher

verified publisheraryak.dev

Weekly Downloads

A fully customizable Flutter tab bar with infinite scrolling capability. Create horizontally scrollable tab bars where tabs repeat seamlessly in both directions.

Repository (GitHub)
View/report issues

Topics

#tabbar #tabs #infinite-scroll #scrollable #widget

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on infinite_scrollable_tabbar