infinite_scroller 1.0.0 copy "infinite_scroller: ^1.0.0" to clipboard
infinite_scroller: ^1.0.0 copied to clipboard

A high-performance, infinitely scrolling horizontal grid widget for Flutter. Features synchronized tabs, haptic feedback, controller support, and extensive customization.

example/main.dart

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

void main() => runApp(const DemoApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        fontFamily: 'Inter',
        colorSchemeSeed: const Color(0xFFE91E63),
      ),
      home: const MarketHomeScreen(),
    );
  }
}

/// Example item model
class ShopItem {
  final String id;
  final String title;
  final String? imageUrl;
  final Color bgColor;
  final double? price;

  const ShopItem({
    required this.id,
    required this.title,
    this.imageUrl,
    required this.bgColor,
    this.price,
  });
}

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

  @override
  State<MarketHomeScreen> createState() => _MarketHomeScreenState();
}

class _MarketHomeScreenState extends State<MarketHomeScreen> {
  late final ScrollerController _controller;
  int _currentIndex = 0;
  bool _isRtl = false;

  // IMPORTANT: Cache sections list to prevent unnecessary rebuilds
  // Using a getter that creates a new list each time would cause performance issues
  late final List<ScrollerSection<ShopItem>> _sections;

  @override
  void initState() {
    super.initState();
    _controller = ScrollerController();
    _sections = _buildSections();
  }

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

  List<ScrollerSection<ShopItem>> _buildSections() => [
        const ScrollerSection(
          id: 'for-you',
          label: 'For You',
          icon: Icons.favorite,
          items: [
            ShopItem(id: '1', title: 'Top Picks', bgColor: Color(0xFFFDEFF2)),
            ShopItem(id: '2', title: 'Deals', bgColor: Color(0xFFFDEFF2)),
            ShopItem(id: '3', title: 'Weekly Offers', bgColor: Color(0xFFFDEFF2)),
            ShopItem(id: '4', title: 'New Arrivals', bgColor: Color(0xFFFDEFF2)),
            ShopItem(id: '5', title: 'Best Sellers', bgColor: Color(0xFFFDEFF2)),
            ShopItem(id: '6', title: 'Trending Now', bgColor: Color(0xFFFDEFF2)),
          ],
        ),
        const ScrollerSection(
          id: 'fresh',
          label: 'Fresh',
          icon: Icons.set_meal,
          items: [
            ShopItem(id: '7', title: 'Fruits & Vegetables', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '8', title: 'Meat & Seafood', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '9', title: 'Deli Counter', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '10', title: 'Bakery', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '11', title: 'Dairy & Eggs', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '12', title: 'Milk & Alternatives', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '13', title: 'Organic Produce', bgColor: Color(0xFFF9F4F6)),
            ShopItem(id: '14', title: 'Fresh Salads', bgColor: Color(0xFFF9F4F6)),
          ],
        ),
        const ScrollerSection(
          id: 'groceries',
          label: 'Groceries',
          icon: Icons.icecream_outlined,
          items: [
            ShopItem(id: '15', title: 'Snacks', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '16', title: 'Beverages', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '17', title: 'Frozen Foods', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '18', title: 'Canned Goods', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '19', title: 'Pasta & Rice', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '20', title: 'Cereals', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '21', title: 'Condiments', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '22', title: 'Cooking Oils', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '23', title: 'Spices & Herbs', bgColor: Color(0xFFFEF9E7)),
            ShopItem(id: '24', title: 'Baking Supplies', bgColor: Color(0xFFFEF9E7)),
          ],
        ),
        const ScrollerSection(
          id: 'beverages',
          label: 'Beverages',
          icon: Icons.local_cafe,
          items: [
            ShopItem(id: '25', title: 'Coffee', bgColor: Color(0xFFE8F6F3)),
            ShopItem(id: '26', title: 'Tea', bgColor: Color(0xFFE8F6F3)),
            ShopItem(id: '27', title: 'Juices', bgColor: Color(0xFFE8F6F3)),
            ShopItem(id: '28', title: 'Soft Drinks', bgColor: Color(0xFFE8F6F3)),
            ShopItem(id: '29', title: 'Water', bgColor: Color(0xFFE8F6F3)),
            ShopItem(id: '30', title: 'Energy Drinks', bgColor: Color(0xFFE8F6F3)),
          ],
        ),
        const ScrollerSection(
          id: 'health',
          label: 'Health',
          icon: Icons.health_and_safety,
          items: [
            ShopItem(id: '31', title: 'Vitamins', bgColor: Color(0xFFFDF2E9)),
            ShopItem(id: '32', title: 'Supplements', bgColor: Color(0xFFFDF2E9)),
            ShopItem(id: '33', title: 'First Aid', bgColor: Color(0xFFFDF2E9)),
            ShopItem(id: '34', title: 'Pain Relief', bgColor: Color(0xFFFDF2E9)),
            ShopItem(id: '35', title: 'Allergy', bgColor: Color(0xFFFDF2E9)),
            ShopItem(id: '36', title: 'Digestive Health', bgColor: Color(0xFFFDF2E9)),
          ],
        ),
        const ScrollerSection(
          id: 'beauty',
          label: 'Beauty',
          icon: Icons.spa,
          items: [
            ShopItem(id: '37', title: 'Skincare', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '38', title: 'Hair Care', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '39', title: 'Makeup', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '40', title: 'Fragrances', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '41', title: 'Body Care', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '42', title: 'Nail Care', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '43', title: 'Men\'s Grooming', bgColor: Color(0xFFF5EEF8)),
            ShopItem(id: '44', title: 'Bath & Shower', bgColor: Color(0xFFF5EEF8)),
          ],
        ),
        const ScrollerSection(
          id: 'home',
          label: 'Home',
          icon: Icons.local_laundry_service,
          items: [
            ShopItem(id: '45', title: 'Cleaning', bgColor: Color(0xFFEBF5FB)),
            ShopItem(id: '46', title: 'Paper Products', bgColor: Color(0xFFEBF5FB)),
            ShopItem(id: '47', title: 'Laundry', bgColor: Color(0xFFEBF5FB)),
            ShopItem(id: '48', title: 'Air Fresheners', bgColor: Color(0xFFEBF5FB)),
            ShopItem(id: '49', title: 'Trash Bags', bgColor: Color(0xFFEBF5FB)),
            ShopItem(id: '50', title: 'Kitchen Tools', bgColor: Color(0xFFEBF5FB)),
          ],
        ),
        const ScrollerSection(
          id: 'pets',
          label: 'Pets',
          icon: Icons.pets,
          items: [
            ShopItem(id: '51', title: 'Dog Food', bgColor: Color(0xFFE8DAEF)),
            ShopItem(id: '52', title: 'Cat Food', bgColor: Color(0xFFE8DAEF)),
            ShopItem(id: '53', title: 'Pet Treats', bgColor: Color(0xFFE8DAEF)),
            ShopItem(id: '54', title: 'Pet Toys', bgColor: Color(0xFFE8DAEF)),
            ShopItem(id: '55', title: 'Pet Care', bgColor: Color(0xFFE8DAEF)),
            ShopItem(id: '56', title: 'Pet Accessories', bgColor: Color(0xFFE8DAEF)),
          ],
        ),
      ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: _buildAppBar(),
      body: InfiniteScroller<ShopItem>(
        sections: _sections,
        controller: _controller,
        initialIndex: 0,

        // RTL/LTR support - toggle with the button in the app bar
        textDirection: _isRtl ? TextDirection.rtl : TextDirection.ltr,

        // Tab bar configuration
        tabBarConfig: const TabBarConfig(
          height: 110,
          tabWidth: 95,
          animationDuration: Duration(milliseconds: 400),
          animationCurve: Curves.easeOutCubic,
        ),

        // Grid configuration
        gridConfig: const GridConfig(
          crossAxisCount: 2,
          columnWidth: 180,
          mainAxisSpacing: 16,
          crossAxisSpacing: 16,
          childAspectRatio: 1.1,
          scrollDuration: Duration(milliseconds: 600),
          scrollCurve: Curves.easeInOutQuart,
        ),

        // Haptic configuration
        hapticConfig: const HapticConfig(
          enabled: true,
          onSectionChange: HapticType.light,
          onTabTap: HapticType.selection,
        ),

        // Callbacks
        onSectionChanged: (index, sectionId) {
          setState(() => _currentIndex = index);
          debugPrint('Section changed: $index ($sectionId)');
        },

        onItemTap: (item, sectionIndex, itemIndex) {
          _showItemDetails(context, item);
        },

        // Tab builder
        tabBuilder: (context, section, index, isActive) {
          return _SectionTab(section: section, isActive: isActive);
        },

        // Item builder
        itemBuilder: (context, section, item, itemIndex, globalIndex) {
          return _ItemCard(item: item);
        },
      ),
      floatingActionButton: _buildNavigationFab(),
    );
  }

  PreferredSizeWidget _buildAppBar() {
    return AppBar(
      backgroundColor: Colors.white,
      surfaceTintColor: Colors.white,
      toolbarHeight: 80,
      title: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Shop by category',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24, color: Colors.black),
          ),
          Text(
            'Viewing: ${_sections[_currentIndex].label}',
            style: const TextStyle(fontSize: 14, color: Colors.grey),
          ),
        ],
      ),
      actions: [
        // RTL/LTR toggle button
        IconButton(
          onPressed: () => setState(() => _isRtl = !_isRtl),
          icon: Icon(
            _isRtl ? Icons.format_textdirection_r_to_l : Icons.format_textdirection_l_to_r,
            color: const Color(0xFFE91E63),
          ),
          tooltip: _isRtl ? 'Switch to LTR' : 'Switch to RTL',
        ),
        Padding(
          padding: const EdgeInsets.only(right: 8.0),
          child: TextButton(
            onPressed: () {},
            child: const Row(
              children: [
                Text(
                  'View All',
                  style: TextStyle(color: Color(0xFFE91E63), fontWeight: FontWeight.bold, fontSize: 16),
                ),
                Icon(Icons.chevron_right, color: Color(0xFFE91E63)),
              ],
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildNavigationFab() {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        FloatingActionButton.small(
          heroTag: 'prev',
          onPressed: () {
            final newIndex = (_currentIndex - 1).clamp(0, _sections.length - 1);
            _controller.animateTo(newIndex);
          },
          child: const Icon(Icons.arrow_back),
        ),
        const SizedBox(height: 8),
        FloatingActionButton.small(
          heroTag: 'next',
          onPressed: () {
            final newIndex = (_currentIndex + 1).clamp(0, _sections.length - 1);
            _controller.animateTo(newIndex);
          },
          child: const Icon(Icons.arrow_forward),
        ),
      ],
    );
  }

  void _showItemDetails(BuildContext context, ShopItem item) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Container(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              item.title,
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text('Item ID: ${item.id}'),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: FilledButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('Close'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Section tab widget
class _SectionTab extends StatelessWidget {
  final ScrollerSection<ShopItem> section;
  final bool isActive;

  const _SectionTab({
    required this.section,
    required this.isActive,
  });

  @override
  Widget build(BuildContext context) {
    const activeColor = Color(0xFFE91E63);

    return SizedBox(
      width: 95,
      child: Container(
        decoration: BoxDecoration(
          color: isActive ? activeColor.withOpacity(0.05) : Colors.transparent,
          border: Border(
            bottom: BorderSide(
              color: isActive ? activeColor : Colors.transparent,
              width: 3,
            ),
          ),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              padding: const EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(16),
                border: Border.all(
                  color: isActive ? activeColor : Colors.grey.shade200,
                  width: 1.5,
                ),
              ),
              child: Icon(
                section.icon,
                color: isActive ? activeColor : Colors.black87,
                size: 28,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              section.label,
              textAlign: TextAlign.center,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              style: TextStyle(
                fontSize: 11,
                fontWeight: isActive ? FontWeight.bold : FontWeight.w500,
                color: isActive ? activeColor : Colors.grey.shade700,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Item card widget
class _ItemCard extends StatelessWidget {
  final ShopItem item;

  const _ItemCard({required this.item});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: Container(
            width: double.infinity,
            decoration: BoxDecoration(
              color: item.bgColor,
              borderRadius: BorderRadius.circular(24),
            ),
            child: item.imageUrl != null && item.imageUrl!.isNotEmpty
                ? ClipRRect(
                    borderRadius: BorderRadius.circular(24),
                    child: Image.network(
                      item.imageUrl!,
                      fit: BoxFit.cover,
                      errorBuilder: (_, __, ___) => _buildPlaceholder(),
                    ),
                  )
                : _buildPlaceholder(),
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4),
          child: Text(
            item.title,
            textAlign: TextAlign.center,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            style: const TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 14,
              color: Colors.black87,
              height: 1.1,
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildPlaceholder() {
    return const Center(
      child: Icon(Icons.image_outlined, size: 50, color: Colors.black12),
    );
  }
}
0
likes
160
points
624
downloads

Publisher

unverified uploader

Weekly Downloads

A high-performance, infinitely scrolling horizontal grid widget for Flutter. Features synchronized tabs, haptic feedback, controller support, and extensive customization.

Homepage
Repository (GitHub)
View/report issues

Topics

#infinite-scroll #grid #horizontal-scroll #widget #tabs

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on infinite_scroller