infinite_scroller 1.1.0
infinite_scroller: ^1.1.0 copied to clipboard
A high-performance, infinitely scrolling horizontal grid widget for Flutter. Features synchronized tabs, haptic feedback, controller support, and extensive customization.
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.