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.
example/lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hexsis_infinity_scroll/hexsis_infinity_scroll.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hexsis Infinity Scroll Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Infinity Scroll Examples'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_ExampleCard(
title: 'Simple ListView',
description: 'Auto-detect pagination',
onTap:
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SimpleListExample(),
),
),
),
_ExampleCard(
title: 'GridView',
description: 'Grid layout with infinite scroll',
onTap:
() => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const GridExample()),
),
),
_ExampleCard(
title: 'Sliver Example',
description: 'CustomScrollView with sliver widgets',
onTap:
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SliverExample(),
),
),
),
_ExampleCard(
title: 'Controller API',
description: 'Manual control with controller',
onTap:
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ControllerExample(),
),
),
),
],
),
);
}
}
class _ExampleCard extends StatelessWidget {
const _ExampleCard({
required this.title,
required this.description,
required this.onTap,
});
final String title;
final String description;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(description),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: onTap,
),
);
}
}
class SimpleListExample extends StatelessWidget {
const SimpleListExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Simple ListView')),
body: InfiniteScrollListView<Product>(
simpleFetcher: (page) async => ProductApi.fetchProducts(page),
itemBuilder: (context, product, index) {
return ProductListTile(product: product, index: index);
},
separatorBuilder: (context, index) => const Divider(height: 1),
config: const InfiniteScrollConfig(pageSize: 20),
),
);
}
}
class GridExample extends StatelessWidget {
const GridExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GridView')),
body: InfiniteScrollGridView<Product>(
simpleFetcher: (page) async => ProductApi.fetchProducts(page),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemBuilder: (context, product, index) {
return ProductGridCard(product: product);
},
config: const InfiniteScrollConfig(pageSize: 20),
),
);
}
}
class SliverExample extends StatefulWidget {
const SliverExample({super.key});
@override
State<SliverExample> createState() => _SliverExampleState();
}
class _SliverExampleState extends State<SliverExample> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Sliver Example'),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
),
),
),
),
),
InfiniteScrollSliverList<Product>(
scrollController: _scrollController,
simpleFetcher: (page) async => ProductApi.fetchProducts(page),
itemBuilder: (context, product, index) {
return ProductListTile(product: product, index: index);
},
config: const InfiniteScrollConfig(pageSize: 20),
),
],
),
);
}
}
class ControllerExample extends StatefulWidget {
const ControllerExample({super.key});
@override
State<ControllerExample> createState() => _ControllerExampleState();
}
class _ControllerExampleState extends State<ControllerExample> {
late InfiniteScrollController<Product> _controller;
@override
void initState() {
super.initState();
_controller = InfiniteScrollController<Product>(
simpleFetcher: (page) async => ProductApi.fetchProducts(page),
config: const InfiniteScrollConfig(pageSize: 20),
);
_controller.addListener(() => setState(() {}));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Controller API'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Controller Info'),
content: Text(
'Items: ${_controller.items.length}\n'
'Page: ${_controller.currentPage}\n'
'Has More: ${_controller.hasMore}\n'
'Loading: ${_controller.isLoading}\n'
'Loading More: ${_controller.isLoadingMore}',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
},
),
],
),
body: ListView.builder(
controller: _controller.scrollController,
itemCount:
_controller.items.length + (_controller.isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _controller.items.length) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
);
}
return ProductListTile(
product: _controller.items[index],
index: index,
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _controller.canLoadMore ? _controller.loadMore : null,
icon: const Icon(Icons.download),
label: const Text('Load More'),
),
);
}
}
class Product {
final int id;
final String name;
final String description;
final double price;
final String imageUrl;
Product({
required this.id,
required this.name,
required this.description,
required this.price,
required this.imageUrl,
});
}
class ProductApi {
static Future<List<Product>> fetchProducts(int page) async {
await Future.delayed(const Duration(milliseconds: 800));
if (page > 10) {
return [];
}
return List.generate(20, (index) {
final id = (page - 1) * 20 + index + 1;
return Product(
id: id,
name: 'Product #$id',
description: 'Description for product $id',
price: Random().nextDouble() * 100 + 10,
imageUrl: 'https://picsum.photos/200?random=$id',
);
});
}
}
class ProductListTile extends StatelessWidget {
const ProductListTile({
required this.product,
required this.index,
super.key,
});
final Product product;
final int index;
@override
Widget build(BuildContext context) {
return ListTile(
leading: CircleAvatar(child: Text('#${index + 1}')),
title: Text(product.name),
subtitle: Text(product.description),
trailing: Text(
'\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
);
}
}
class ProductGridCard extends StatelessWidget {
const ProductGridCard({required this.product, super.key});
final Product product;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Container(
color: Colors.primaries[product.id % Colors.primaries.length],
child: Center(
child: Text(
'#${product.id}',
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
}
}