infinite_scrollable_tabbar 0.0.1
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.
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),
),
),
],
),
),
),
],
);
}
}