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.0.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');
  },
  
  // ... other properties
)

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
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.