Infinite Scroller
A high-performance, infinitely scrolling horizontal grid widget for Flutter. Perfect for e-commerce apps, media galleries, and any app that needs to display grouped content with smooth navigation.
Features
- Infinite Horizontal Scrolling - Seamless looping with no visible reset
- Synchronized Tabs - Auto-select based on scroll position
- Controller Support - Programmatic navigation and state access
- Highly Customizable - Builders and configuration objects for full control
- Haptic Feedback - Configurable feedback on interactions
- Performance Optimized - ValueNotifier-based efficient rebuilds
- Generic Types - Works with any data model
- Loading & Empty States - Built-in support for common UI states
- RTL/LTR Support - Full right-to-left and left-to-right language support
Installation
Add to your pubspec.yaml:
dependencies:
infinite_scroller: ^1.1.0
Or install via command line:
flutter pub add infinite_scroller
Quick Start
import 'package:infinite_scroller/infinite_scroller.dart';
InfiniteScroller<Product>(
sections: [
ScrollerSection(
id: 'electronics',
label: 'Electronics',
icon: Icons.devices,
items: [Product('iPhone'), Product('MacBook')],
),
ScrollerSection(
id: 'clothing',
label: 'Clothing',
icon: Icons.checkroom,
items: [Product('T-Shirt'), Product('Jeans')],
),
],
tabBuilder: (context, section, index, isActive) {
return MyTab(
label: section.label,
icon: section.icon,
isActive: isActive,
);
},
itemBuilder: (context, section, item, itemIndex, globalIndex) {
return ProductCard(product: item);
},
)
Configuration
TabBarConfig
Control the appearance and behavior of the tab bar:
TabBarConfig(
height: 110.0, // Tab bar height
tabWidth: 100.0, // Width of each tab
backgroundColor: Colors.white,
showShadow: true,
shadowElevation: 2.0,
animationDuration: Duration(milliseconds: 400),
animationCurve: Curves.easeOutCubic,
centerTabs: false,
)
GridConfig
Configure the scrolling grid layout:
GridConfig(
crossAxisCount: 2, // Number of rows
columnWidth: 200.0, // Width per column
mainAxisSpacing: 12.0, // Horizontal spacing
crossAxisSpacing: 12.0, // Vertical spacing
childAspectRatio: 1.1, // Item aspect ratio
padding: EdgeInsets.all(16),
scrollDuration: Duration(milliseconds: 600),
scrollCurve: Curves.easeInOutQuart,
loopFactor: 10, // Infinite loop multiplier
)
HapticConfig
Customize haptic feedback:
HapticConfig(
enabled: true,
onSectionChange: HapticType.light,
onTabTap: HapticType.selection,
)
Available haptic types: light, medium, heavy, selection, none
Controller
Use ScrollerController for programmatic control:
final controller = ScrollerController();
// Animate to a section
await controller.animateTo(2);
// Jump without animation
controller.jumpTo(0);
// Scroll the grid
controller.scrollBy(100.0);
// Access state
print('Active section: ${controller.activeIndex}');
print('Grid offset: ${controller.gridOffset}');
print('Is attached: ${controller.isAttached}');
Callbacks
React to user interactions:
InfiniteScroller<Product>(
// Called when active section changes
onSectionChanged: (index, sectionId) {
print('Now viewing: $sectionId');
},
// Called when an item is tapped
onItemTap: (item, sectionIndex, itemIndex) {
Navigator.push(context, ProductDetailPage(item));
},
// Called during scroll
onScroll: (offset, maxExtent) {
print('Scroll: $offset / $maxExtent');
},
// Called when a tab is double-tapped
onTabDoubleTap: (section, index) {
print('Double tapped: ${section.label}');
},
// Called when a tab is long-pressed
onTabLongPress: (section, index) {
showContextMenu(context, section);
},
// ... other properties
)
Tab Gesture Callbacks
The scroller supports additional gesture callbacks on section tabs for enhanced interactions:
InfiniteScroller<Product>(
// Double-tap to perform quick actions
onTabDoubleTap: (section, index) {
toggleFavorite(section);
},
// Long-press to show context menu
onTabLongPress: (section, index) {
showModalBottomSheet(
context: context,
builder: (_) => TabOptionsMenu(section: section),
);
},
// ...
)
Note: Adding onDoubleTap introduces a slight delay before single taps are recognized (to distinguish between single and double taps). Only add this callback if you need the functionality.
ScrollerSection
Define your sections with full flexibility:
ScrollerSection<Product>(
id: 'unique-id', // Optional unique identifier
label: 'Section Name', // Display label
items: [...], // List of items
icon: Icons.star, // Optional icon
iconWidget: CustomIcon(), // Or use a custom widget
data: {'priority': 1}, // Optional arbitrary data
enabled: true, // Can be disabled
)
Advanced Usage
Custom Detection Point
Control where section detection happens on screen:
InfiniteScroller(
detectionPoint: 0.4, // 0.0 = left, 0.5 = center, 1.0 = right
// ...
)
Tab Bar Position
Place the tab bar at top or bottom:
InfiniteScroller(
showTabBar: true,
tabBarPosition: TabBarPosition.bottom,
// ...
)
Loading & Empty States
Handle loading and empty data gracefully:
InfiniteScroller(
isLoading: isDataLoading,
loadingWidget: CustomLoadingSpinner(),
emptyWidget: EmptyStateWidget(message: 'No content'),
// ...
)
Initial Section
Start at a specific section:
InfiniteScroller(
initialIndex: 2, // Start at third section
// ...
)
RTL/LTR Support
The scroller fully supports right-to-left (RTL) and left-to-right (LTR) text directions. This is essential for apps supporting Arabic, Hebrew, Persian, and other RTL languages.
// Explicit RTL mode
InfiniteScroller(
textDirection: TextDirection.rtl,
// ...
)
// Explicit LTR mode
InfiniteScroller(
textDirection: TextDirection.ltr,
// ...
)
// Inherit from context (default behavior)
InfiniteScroller(
// textDirection: null (uses Directionality from context)
// ...
)
When RTL is enabled:
- Items scroll from right to left
- Tabs are arranged right to left
- Section detection point is automatically mirrored
- All scroll animations work correctly in the reversed direction
For apps that need dynamic RTL switching:
bool isRtl = false;
InfiniteScroller(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
// ...
)
Best Practices
Follow these guidelines to get the best performance and user experience from the scroller.
Cache Your Sections List
Always cache the sections list to prevent unnecessary rebuilds and scroll position resets.
// GOOD: Cache sections as a final field
class _MyScreenState extends State<MyScreen> {
late final List<ScrollerSection<Product>> _sections;
@override
void initState() {
super.initState();
_sections = _buildSections();
}
List<ScrollerSection<Product>> _buildSections() => [
ScrollerSection(label: 'Electronics', items: electronics),
// ...
];
@override
Widget build(BuildContext context) {
return InfiniteScroller(sections: _sections, ...);
}
}
// BAD: Creating new list in build() causes problems
Widget build(BuildContext context) {
return InfiniteScroller(
sections: [
ScrollerSection(...), // New list every build!
],
...
);
}
Keep Builders Lightweight
Tab and item builders are called frequently during scroll. Avoid expensive operations.
// GOOD: Simple widget construction
itemBuilder: (context, section, item, itemIndex, globalIndex) {
return ProductCard(product: item);
}
// BAD: Expensive operations in builder
itemBuilder: (context, section, item, itemIndex, globalIndex) {
final processedImage = heavyImageProcessing(item.image); // Slow!
final analytics = computeAnalytics(item); // Slow!
return ProductCard(product: item, image: processedImage);
}
Use ValueNotifier for Scroll Callbacks
The onScroll callback is called on every frame. Never use setState() here.
// GOOD: Use ValueNotifier
final _scrollProgress = ValueNotifier<double>(0);
InfiniteScroller(
onScroll: (offset, maxExtent) {
_scrollProgress.value = offset / maxExtent;
},
...
)
// In your UI:
ValueListenableBuilder<double>(
valueListenable: _scrollProgress,
builder: (context, progress, _) => LinearProgressIndicator(value: progress),
)
// BAD: setState on every scroll frame causes jank
onScroll: (offset, maxExtent) {
setState(() => _progress = offset / maxExtent); // Don't do this!
}
Dispose Controllers
Always dispose the ScrollerController to prevent memory leaks.
class _MyScreenState extends State<MyScreen> {
late final ScrollerController _controller;
@override
void initState() {
super.initState();
_controller = ScrollerController();
}
@override
void dispose() {
_controller.dispose(); // Important!
super.dispose();
}
}
Choose Appropriate Loop Factor
Balance memory usage and scroll smoothness.
GridConfig(
loopFactor: 6, // Default - good for most cases
// loopFactor: 4, // Lower memory, may flicker on very fast scroll
// loopFactor: 10, // Smoother on fast scroll, uses more memory
)
Use Const Constructors
Use const for static configuration objects.
// GOOD: const for better performance
InfiniteScroller(
tabBarConfig: const TabBarConfig(height: 100, tabWidth: 90),
gridConfig: const GridConfig(crossAxisCount: 2, columnWidth: 180),
hapticConfig: const HapticConfig(enabled: true),
...
)
What to Avoid
Common pitfalls that can cause performance issues or unexpected behavior.
Don't Create Sections in Build
Creating new section lists in build() causes the scroller to reinitialize, losing scroll position.
// BAD: This resets scroll position on every rebuild
Widget build(BuildContext context) {
final sections = [
ScrollerSection(label: 'A', items: items),
];
return InfiniteScroller(sections: sections, ...);
}
Don't Use setState in onScroll
This causes the entire widget tree to rebuild on every scroll frame, destroying performance.
// BAD: Causes severe scroll jank
onScroll: (offset, maxExtent) {
setState(() {
_offset = offset;
});
}
Don't Ignore Controller Lifecycle
Forgetting to dispose controllers causes memory leaks.
// BAD: Memory leak!
class _MyScreenState extends State<MyScreen> {
final controller = ScrollerController();
@override
void dispose() {
// Forgot to dispose controller!
super.dispose();
}
}
Don't Use Heavy Widgets in Builders
Complex widgets in builders slow down scrolling.
// BAD: Heavy computation in builder
itemBuilder: (context, section, item, itemIndex, globalIndex) {
// Avoid these in builders:
return FutureBuilder(...); // Async in builder
return StreamBuilder(...); // Stream in builder
return BlocBuilder(...); // Consider pre-computing state
}
// GOOD: Pre-compute and pass data
itemBuilder: (context, section, item, itemIndex, globalIndex) {
return ProductCard(product: item); // Simple widget creation
}
Don't Set Very Low Loop Factor
Values below 4 may cause visible content jumping on fast scrolls.
// BAD: May cause visual glitches
GridConfig(loopFactor: 2)
// GOOD: Safe minimum
GridConfig(loopFactor: 4)
// RECOMMENDED: Default
GridConfig(loopFactor: 6)
Don't Wrap Items in RepaintBoundary
The grid already adds repaint boundaries. Adding more creates overhead.
// BAD: Redundant repaint boundary
itemBuilder: (context, section, item, itemIndex, globalIndex) {
return RepaintBoundary( // Already handled by grid
child: ProductCard(product: item),
);
}
// GOOD: Just return the widget
itemBuilder: (context, section, item, itemIndex, globalIndex) {
return ProductCard(product: item);
}
Don't Use BouncingScrollPhysics for Grid
Bouncy physics can cause visual artifacts at loop boundaries.
// BAD: Can cause issues at loop boundaries
GridConfig(physics: BouncingScrollPhysics())
// GOOD: Use clamping for smooth infinite scroll
GridConfig(physics: ClampingScrollPhysics()) // Default
Configuration Options Reference
All TabBarConfig Options
| Option | Type | Default | Description |
|---|---|---|---|
height |
double |
110.0 | Tab bar height in pixels |
tabWidth |
double |
100.0 | Width of each tab |
backgroundColor |
Color? |
scaffold bg | Tab bar background |
border |
BoxBorder? |
bottom border | Tab bar border |
padding |
EdgeInsetsGeometry |
zero | Padding around tabs |
showShadow |
bool |
false | Show shadow below |
shadowElevation |
double |
2.0 | Shadow blur radius |
physics |
ScrollPhysics? |
clamping | Tab scroll physics |
centerTabs |
bool |
false | Center tabs (reserved) |
animationDuration |
Duration |
400ms | Tab animation duration |
animationCurve |
Curve |
easeOutCubic | Tab animation curve |
All GridConfig Options
| Option | Type | Default | Description |
|---|---|---|---|
crossAxisCount |
int |
2 | Number of rows |
columnWidth |
double |
200.0 | Width per column |
mainAxisSpacing |
double |
12.0 | Horizontal gap |
crossAxisSpacing |
double |
12.0 | Vertical gap |
childAspectRatio |
double |
1.1 | Item width/height ratio |
padding |
EdgeInsetsGeometry |
all(16) | Grid padding |
physics |
ScrollPhysics? |
clamping | Grid scroll physics |
scrollDuration |
Duration |
600ms | Programmatic scroll duration |
scrollCurve |
Curve |
easeInOutQuart | Programmatic scroll curve |
loopFactor |
int |
6 | Infinite loop multiplier |
All HapticConfig Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
true | Master haptic switch |
onSectionChange |
HapticType |
light | Haptic on section change |
onTabTap |
HapticType |
selection | Haptic on tab tap |
HapticType Values
| Value | Description | iOS Equivalent |
|---|---|---|
light |
Subtle quick tap | UIImpactFeedbackGenerator(.light) |
medium |
Moderate tap | UIImpactFeedbackGenerator(.medium) |
heavy |
Strong noticeable tap | UIImpactFeedbackGenerator(.heavy) |
selection |
UI selection tick | UISelectionFeedbackGenerator |
none |
No haptic feedback | - |
API Reference
InfiniteScroller
| Property | Type | Default | Description |
|---|---|---|---|
sections |
List<ScrollerSection<T>> |
required | List of sections |
tabBuilder |
TabBuilder<T> |
required | Builder for tabs |
itemBuilder |
ItemBuilder<T> |
required | Builder for items |
tabBarConfig |
TabBarConfig |
TabBarConfig() |
Tab bar configuration |
gridConfig |
GridConfig |
GridConfig() |
Grid configuration |
hapticConfig |
HapticConfig |
HapticConfig() |
Haptic configuration |
controller |
ScrollerController? |
null | Optional controller |
onSectionChanged |
OnSectionChanged? |
null | Section change callback |
onItemTap |
OnItemTap<T>? |
null | Item tap callback |
onScroll |
OnScroll? |
null | Scroll callback |
onTabDoubleTap |
OnTabDoubleTap<T>? |
null | Tab double-tap callback |
onTabLongPress |
OnTabLongPress<T>? |
null | Tab long-press callback |
initialIndex |
int |
0 | Starting section |
showTabBar |
bool |
true | Show/hide tab bar |
tabBarPosition |
TabBarPosition |
top | Tab bar position |
isLoading |
bool |
false | Loading state |
loadingWidget |
Widget? |
null | Custom loading widget |
emptyWidget |
Widget? |
null | Custom empty widget |
detectionPoint |
double |
0.4 | Detection point (0.0-1.0) |
textDirection |
TextDirection? |
null | Text direction (RTL/LTR) |
ScrollerSection
| Property | Type | Default | Description |
|---|---|---|---|
id |
String? |
null | Unique identifier |
label |
String |
required | Display label |
items |
List<T> |
required | Items in section |
icon |
IconData? |
null | Section icon |
iconWidget |
Widget? |
null | Custom icon widget |
data |
dynamic |
null | Arbitrary data |
enabled |
bool |
true | Whether selectable |
ScrollerController
| Method/Property | Type | Description |
|---|---|---|
activeIndex |
int |
Current active section index |
isAttached |
bool |
Whether attached to scroller |
gridOffset |
double |
Current grid scroll offset |
tabOffset |
double |
Current tab scroll offset |
animateTo(index) |
Future<void> |
Animate to section |
jumpTo(index) |
void |
Jump to section |
scrollBy(offset) |
void |
Scroll grid by offset |
scrollTo(offset) |
Future<void> |
Animate grid to offset |
License
MIT License - see LICENSE file for details.
Libraries
- infinite_scroller
- A high-performance, infinitely scrolling horizontal grid widget for Flutter.