hexsis_infinity_scroll 0.2.3
hexsis_infinity_scroll: ^0.2.3 copied to clipboard
Production-ready infinite scroll package for Flutter with zero dependencies. Supports ListView, GridView, and Sliver widgets with built-in loading, error, and empty states.
Hexsis Infinity Scroll (Core) #
Zero-dependency infinite scroll package for Flutter with built-in widgets for ListView, GridView, and Sliver support.
Features #
- ✅ Zero dependencies (only Flutter SDK)
- 🎨 4 built-in widgets: ListView, GridView, SliverList, SliverGrid
- 🎯 3 fetcher APIs: Simple, Tuple, or Full control
- 📱 Pull-to-refresh built-in
- 🔄 Auto-loading with configurable threshold
- 🎨 Default UI components: Loading, error, empty states
- ⚡ Debounced scroll detection
- 🛡️ Error handling with retry support
Installation #
dependencies:
hexsis_infinity_scroll: ^0.2.3
Quick Start #
Ultra-Simple (Auto-detect pagination) #
InfiniteScrollListView<Product>(
simpleFetcher: (page) async {
return await api.getProducts(page); // Returns List<Product>
},
itemBuilder: (context, product, index) {
return ProductCard(product);
},
)
With Tuple (Control hasMore) #
InfiniteScrollListView<Product>(
tupleFetcher: (page) async {
final products = await api.getProducts(page);
return (items: products, hasMore: products.length >= 20);
},
itemBuilder: (context, product, index) {
return ProductCard(product);
},
)
Full Control #
InfiniteScrollListView<Product>(
fullFetcher: (page) async {
final response = await api.getProducts(page);
return PaginatedData(
items: response.products,
currentPage: response.page,
totalPages: response.totalPages,
);
},
itemBuilder: (context, product, index) {
return ProductCard(product);
},
)
Widgets #
InfiniteScrollListView #
InfiniteScrollListView<Product>(
simpleFetcher: fetchProducts,
itemBuilder: (context, item, index) => ProductCard(item),
// Optional customization
separatorBuilder: (context, index) => Divider(),
loadingBuilder: (context) => CustomLoadingWidget(),
errorBuilder: (context, error, retry) => CustomErrorWidget(error, retry),
emptyBuilder: (context) => CustomEmptyWidget(),
loadingMoreBuilder: (context) => CustomLoadingMoreWidget(),
config: InfiniteScrollConfig(
threshold: 0.8,
initialPage: 1,
pageSize: 20,
autoLoadOnInit: true,
debounceMilliseconds: 300,
),
)
InfiniteScrollGridView #
InfiniteScrollGridView<Product>(
simpleFetcher: fetchProducts,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, item, index) => ProductCard(item),
)
InfiniteScrollSliverList & InfiniteScrollSliverGrid #
Note: Sliver widgets require an external ScrollController from the parent CustomScrollView.
class MyScreen extends StatefulWidget {
@override
State createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: _scrollController, // Pass to CustomScrollView
slivers: [
SliverAppBar(title: Text('Products')),
InfiniteScrollSliverList<Product>(
scrollController: _scrollController, // Required!
simpleFetcher: fetchProducts,
itemBuilder: (context, item, index) => ProductCard(item),
),
],
);
}
}
For grid layout:
InfiniteScrollSliverGrid<Product>(
scrollController: _scrollController, // Same controller!
simpleFetcher: fetchProducts,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, item, index) => ProductCard(item),
)
Why ScrollController? Sliver widgets in CustomScrollView can't use NotificationListener internally. Passing the parent ScrollController enables reliable scroll detection with minimal boilerplate.
Controller API (Advanced) #
For manual control, use the controller directly:
class ProductListScreen extends StatefulWidget {
@override
State<ProductListScreen> createState() => _ProductListScreenState();
}
class _ProductListScreenState extends State<ProductListScreen> {
late InfiniteScrollController<Product> _controller;
@override
void initState() {
super.initState();
_controller = InfiniteScrollController<Product>(
simpleFetcher: fetchProducts,
config: InfiniteScrollConfig(threshold: 0.8),
);
_controller.addListener(() => setState(() {}));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller.scrollController,
itemCount: _controller.items.length,
itemBuilder: (context, index) {
return ProductCard(_controller.items[index]);
},
);
}
}
Configuration #
InfiniteScrollConfig(
threshold: 0.8, // Percentage for small lists
thresholdPixels: 500.0, // Fixed pixels from bottom for large lists
thresholdBreakpoint: 5000.0, // Switch point between strategies
useSmartThreshold: true, // Enable smart threshold logic
initialPage: 1, // Starting page
pageSize: 20, // Items per page (for auto-detection)
enablePullToRefresh: true, // Enable pull-to-refresh
autoLoadOnInit: true, // Auto-load first page
debounceMilliseconds: 300, // Debounce scroll events
)
Smart Threshold Logic #
By default, the package uses smart threshold logic to prevent premature loading in long lists:
- Small lists (<5000px): Uses percentage (default: 80%)
- Large lists (≥5000px): Uses fixed pixels from bottom (default: 500px)
This ensures consistent loading behavior regardless of list size.
How It Works #
| List Size | Old Behavior | Smart Threshold | Distance from Bottom |
|---|---|---|---|
| 1000px | 800px (80%) | 800px (80%) | 200px ✅ |
| 5000px | 4000px (80%) | 4500px (fixed) | 500px ✅ |
| 10000px | 8000px (80%) | 9500px (fixed) | 500px ✅ |
| 20000px | 16000px (80%) | 19500px (fixed) | 500px ✅ |
Why this matters: With the old approach, a 20,000px list would start loading when you're still 4,000px away - wasting API calls and loading data the user will never see. Smart threshold keeps it consistent at 500px.
Customize Smart Threshold #
InfiniteScrollConfig(
thresholdPixels: 300.0, // Closer to bottom
thresholdBreakpoint: 3000.0, // Earlier switch point
)
Disable Smart Threshold #
InfiniteScrollConfig(
useSmartThreshold: false, // Use classic percentage-only behavior
)
Extension Packages #
For state management integration:
- hexsis_infinity_scroll_bloc - BLoC/Cubit support
- hexsis_infinity_scroll_riverpod - Riverpod support
- hexsis_infinity_scroll_hooks - Hooks wrappers
Reliability #
Pending fetches are safely ignored when their controller or widget is disposed. The package is covered by behavioral tests for pagination strategies, end-of-data detection, retries, errors, duplicate request prevention, refresh, thresholds, and list, grid, and sliver rendering.
License #
MIT License - Copyright (c) 2025 Hexsis Enterprise LLC