infinite_paging_widget 0.1.0
infinite_paging_widget: ^0.1.0 copied to clipboard
A Flutter infinite scroll list/grid with load-more pagination. Supports empty, loading, error, header, and footer slots.
import 'package:flutter/material.dart';
import 'package:infinite_paging_widget/infinite_paging_widget.dart';
void main() {
runApp(const InfinitePagingWidgetDemo());
}
class InfinitePagingWidgetDemo extends StatelessWidget {
const InfinitePagingWidgetDemo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'infinite_paging_widget',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1B4DFF),
brightness: Brightness.light,
),
useMaterial3: true,
scaffoldBackgroundColor: const Color(0xFFF2F4F8),
),
home: const DemoHomePage(),
);
}
}
class DemoHomePage extends StatefulWidget {
const DemoHomePage({super.key});
@override
State<DemoHomePage> createState() => _DemoHomePageState();
}
class _DemoHomePageState extends State<DemoHomePage> {
static const int _pageSize = 20;
static const int _totalItems = 80;
final List<String> _items = [];
var _displayType = PagingDisplayType.list;
var _isLoading = false;
var _hasMore = true;
var _page = 0;
@override
void initState() {
super.initState();
_loadMore();
}
Future<void> _loadMore() async {
if (_isLoading || !_hasMore) return;
setState(() => _isLoading = true);
await Future<void>.delayed(const Duration(milliseconds: 650));
final start = _page * _pageSize;
final end = (start + _pageSize).clamp(0, _totalItems);
final next = [for (var i = start; i < end; i++) 'Item ${i + 1}'];
if (!mounted) return;
setState(() {
_items.addAll(next);
_page++;
_hasMore = _items.length < _totalItems;
_isLoading = false;
});
}
void _reset() {
setState(() {
_items.clear();
_page = 0;
_hasMore = true;
_isLoading = false;
});
_loadMore();
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Infinite Paging'),
centerTitle: false,
backgroundColor: scheme.surface,
surfaceTintColor: Colors.transparent,
actions: [
IconButton(
tooltip: 'Reset',
onPressed: _reset,
icon: const Icon(Icons.refresh),
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Text(
'Scroll near the bottom to load the next fake page '
'(${_items.length}/$_totalItems).',
style: Theme.of(
context,
).textTheme.bodyLarge?.copyWith(color: scheme.onSurfaceVariant),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SegmentedButton<PagingDisplayType>(
segments: const [
ButtonSegment(
value: PagingDisplayType.list,
label: Text('List'),
icon: Icon(Icons.view_list_outlined),
),
ButtonSegment(
value: PagingDisplayType.grid,
label: Text('Grid'),
icon: Icon(Icons.grid_view_outlined),
),
],
selected: {_displayType},
onSelectionChanged: (value) {
setState(() => _displayType = value.first);
},
),
),
const SizedBox(height: 8),
Expanded(
child: InfinitePagingWidget<String>(
items: _items,
displayType: _displayType,
hasMore: _hasMore,
isLoading: _isLoading,
onLoadMore: _loadMore,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
gridCrossAxisCount: 2,
gridMainAxisSpacing: 12,
gridCrossAxisSpacing: 12,
gridChildAspectRatio: 1.15,
separatorBuilder: _displayType == PagingDisplayType.list
? (context, index) => const SizedBox(height: 8)
: null,
itemBuilder: (context, item, index) {
if (_displayType == PagingDisplayType.grid) {
return _GridCard(label: item, index: index);
}
return _ListCard(label: item, index: index);
},
footer: !_hasMore && _items.isNotEmpty
? Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Text(
'End of results',
textAlign: TextAlign.center,
style: TextStyle(color: scheme.onSurfaceVariant),
),
)
: null,
),
),
],
),
);
}
}
class _ListCard extends StatelessWidget {
const _ListCard({required this.label, required this.index});
final String label;
final int index;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Card(
elevation: 0,
color: scheme.surfaceContainerHighest.withValues(alpha: 0.55),
child: ListTile(
leading: CircleAvatar(
backgroundColor: scheme.primaryContainer,
child: Text('${index + 1}'),
),
title: Text(label),
subtitle: const Text('Fake paginated row'),
),
);
}
}
class _GridCard extends StatelessWidget {
const _GridCard({required this.label, required this.index});
final String label;
final int index;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
color: scheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: scheme.outlineVariant),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor: scheme.primaryContainer,
child: Text('${index + 1}'),
),
const SizedBox(height: 10),
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
],
),
);
}
}