infinite_scroller 1.0.0
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.
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),
);
}
}