flutter_paginatrix 1.0.3
flutter_paginatrix: ^1.0.3 copied to clipboard
A simple, backend-agnostic pagination engine with UI widgets for Flutter
π Flutter Paginatrix #
A production-ready, backend-agnostic pagination engine for Flutter
Flutter Paginatrix is a comprehensive, type-safe pagination library that works with any backend (REST, GraphQL, Firebase) and supports multiple pagination strategies. Built with performance, reliability, and developer experience in mind.
π Table of Contents #
- Why Flutter Paginatrix?
- Features
- Installation
- Quick Start
- Core Concepts
- Widgets & Components
- Advanced Usage
- Examples
- API Overview
- Best Practices
- Troubleshooting
- Contributing
- License
π― Why Flutter Paginatrix? #
The Problem #
Building pagination in Flutter typically requires:
- Managing loading states manually
- Handling errors and retries
- Implementing infinite scroll logic
- Parsing different API response formats
- Managing cache and request cancellation
- Writing boilerplate code for every list
The Solution #
Flutter Paginatrix provides:
- β Zero boilerplate - Get started in minutes
- β Backend-agnostic - Works with any API structure
- β Type-safe - Full generics support with compile-time safety
- β Production-ready - 171+ tests, comprehensive error handling
- β Beautiful UI - Pre-built widgets with customizable loaders
- β High performance - LRU caching, request cancellation, debouncing
- β
Web support - Includes
PageSelectorfor web applications
β¨ Features #
Core Features #
- π― Backend-Agnostic - Works with any API structure (REST, GraphQL, Firebase)
- π Multiple Strategies - Page-based, offset-based, and cursor-based pagination
- π¨ UI Components -
PaginatrixListView&PaginatrixGridViewwith Sliver support - β‘ High Performance - LRU caching, request cancellation, and debouncing
- π‘οΈ Robust - Race condition protection and automatic retries
- π Search & Filter - Built-in support for searching, filtering, and sorting
- π± Web Support - Includes
PageSelectorfor web apps - π Multiple Loaders - 5+ beautiful loader animations
- π¨ Customizable - Extensive customization options for all widgets
- π Pull-to-Refresh - Built-in pull-to-refresh support
- π State Management - Works with BLoC, Provider, Riverpod, or standalone
Technical Features #
- Type-Safe - Full generics support with compile-time type checking
- Memory Efficient - Automatic request cancellation and cache management
- Error Handling - 6 error types with automatic retry logic
- Meta Parsers - Pre-configured parsers for common API formats
- Custom Parsers - Support for any custom API response structure
- Testing - Comprehensive test suite (171+ tests)
- Documentation - Complete API documentation with examples
π¦ Installation #
Add this to your package's pubspec.yaml file:
dependencies:
flutter_paginatrix: ^1.0.3
Then run:
flutter pub get
Note: Replace ^1.0.3 with the latest version from pub.flutter-io.cn.
π Quick Start #
1. Create a Controller #
import 'package:flutter_paginatrix/flutter_paginatrix.dart';
final controller = PaginatrixController<User>(
loader: ({page, perPage, query, cancelToken}) async {
final response = await api.getUsers(
page: page,
perPage: perPage,
search: query?.searchTerm,
);
return response.data; // {data: [...], meta: {...}}
},
itemDecoder: User.fromJson,
metaParser: ConfigMetaParser(MetaConfig.nestedMeta),
);
2. Use the Widget #
PaginatrixListView<User>(
controller: controller,
itemBuilder: (context, user, index) {
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
)
3. Initialize in Your Widget #
@override
void initState() {
super.initState();
controller.loadFirstPage(); // Required
}
@override
void dispose() {
controller.close(); // Required
super.dispose();
}
That's it! The widget automatically handles:
- β Loading states
- β Error states with retry
- β Empty states
- β Infinite scroll pagination
- β Pull-to-refresh
- β Request cancellation
π§ Core Concepts #
Pagination Strategies #
Flutter Paginatrix supports three pagination strategies:
-
Page-based - Uses
pageandper_pageparameters// API: GET /users?page=1&per_page=20 -
Offset-based - Uses
offsetandlimitparameters// API: GET /users?offset=0&limit=20 -
Cursor-based - Uses
cursorortokenparameters// API: GET /users?cursor=abc123
Meta Parsers #
Meta parsers extract pagination metadata from API responses:
Nested Meta Format (most common):
{
"data": [...],
"meta": {
"page": 1,
"per_page": 20,
"total": 100,
"last_page": 5
}
}
Results Format:
{
"results": [...],
"page": 1,
"per_page": 20,
"total": 100
}
Custom Format:
metaParser: CustomMetaParser((data) {
return {
'items': data['products'],
'meta': {
'page': data['currentPage'],
'hasMore': data['hasNext'],
},
};
}),
State Management #
Flutter Paginatrix provides two APIs:
-
PaginatrixController (Recommended) - Simple, clean API
final controller = PaginatrixController<User>(...); -
PaginatrixCubit - For BLoC pattern integration
final cubit = PaginatrixCubit<User>(...);
Both APIs are functionally equivalent. Use PaginatrixController for simplicity, or PaginatrixCubit if you're already using BLoC.
π¨ Widgets & Components #
Main Widgets #
PaginatrixListView
A ListView widget with built-in pagination support.
PaginatrixListView<User>(
controller: controller,
itemBuilder: (context, user, index) => UserTile(user: user),
separatorBuilder: (context, index) => Divider(), // Optional
padding: EdgeInsets.all(16), // Optional
physics: BouncingScrollPhysics(), // Optional
)
Key Features:
- Infinite scroll pagination
- Pull-to-refresh support
- Customizable loading states
- Error handling with retry
- Empty state handling
- Sliver-based for optimal performance
PaginatrixGridView
A GridView widget with built-in pagination support.
PaginatrixGridView<Product>(
controller: controller,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, product, index) => ProductCard(product: product),
)
Key Features:
- Same features as PaginatrixListView
- Grid layout support
- Responsive grid delegates
- Custom grid spacing
Loader Widgets #
AppendLoader
Displays a loading indicator when loading more items.
AppendLoader(
loaderType: LoaderType.bouncingDots, // or wave, rotatingSquares, pulse, skeleton
message: 'Loading more...',
color: Colors.blue,
size: 40.0,
)
Available Loader Types:
LoaderType.bouncingDots- Animated bouncing dotsLoaderType.wave- Wave animationLoaderType.rotatingSquares- Rotating squaresLoaderType.pulse- Pulsing circleLoaderType.skeleton- Skeleton loaderLoaderType.traditional- Traditional CircularProgressIndicator
Modern Loaders
Standalone loader widgets for custom use cases:
BouncingDotsLoader(
color: Colors.blue,
size: 8.0,
message: 'Loading...',
)
WaveLoader(
color: Colors.blue,
size: 40.0,
message: 'Loading...',
)
RotatingSquaresLoader(
color: Colors.blue,
size: 30.0,
)
PulseLoader(
color: Colors.blue,
size: 50.0,
)
SkeletonLoader(
color: Colors.blue,
itemCount: 5,
message: 'Loading...',
)
Empty State Widgets #
PaginatrixEmptyView
Base empty state widget (fully customizable):
PaginatrixEmptyView(
icon: Icon(Icons.inbox_outlined),
title: 'No items found',
description: 'Try adjusting your search or filters',
action: ElevatedButton(
onPressed: () => controller.refresh(),
child: Text('Refresh'),
),
)
Predefined Empty Views
PaginatrixSearchEmptyView - For search results:
PaginatrixSearchEmptyView(
searchTerm: 'john',
onClearSearch: () => controller.clearSearch(),
)
PaginatrixNetworkEmptyView - For network errors:
PaginatrixNetworkEmptyView(
onRetry: () => controller.retry(),
)
PaginatrixGenericEmptyView - General purpose:
PaginatrixGenericEmptyView(
message: 'No items available',
onRefresh: () => controller.refresh(),
)
Error Widgets #
PaginatrixErrorView
Displays error states with retry functionality:
PaginatrixErrorView(
error: error,
onRetry: () => controller.retry(),
)
PaginatrixAppendErrorView
Error view for append operations:
PaginatrixAppendErrorView(
error: error,
onRetry: () => controller.loadNextPage(),
)
Web Widgets #
PageSelector
Page navigation widget for web applications:
PageSelector(
currentPage: controller.state.meta?.page ?? 1,
totalPages: controller.state.meta?.lastPage ?? 1,
onPageSelected: (page) => controller.loadPage(page),
style: PageSelectorStyle.buttons, // or dropdown, compact
)
Available Styles:
PageSelectorStyle.buttons- Page number buttonsPageSelectorStyle.dropdown- Dropdown selectorPageSelectorStyle.compact- Compact button style
Skeleton Widgets #
PaginatrixSkeletonizer
Skeleton loader for list items:
PaginatrixSkeletonizer(
itemCount: 5,
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(),
title: Container(height: 16, color: Colors.grey),
subtitle: Container(height: 12, color: Colors.grey),
),
)
PaginatrixGridSkeletonizer
Skeleton loader for grid items:
PaginatrixGridSkeletonizer(
itemCount: 6,
crossAxisCount: 2,
itemBuilder: (context, index) => Card(
child: Column(
children: [
Container(height: 100, color: Colors.grey),
Container(height: 16, color: Colors.grey),
],
),
),
)
π₯ Advanced Usage #
Search with Debouncing #
// Search is automatically debounced (400ms default)
controller.updateSearchTerm('john');
// Custom debounce delay
final controller = PaginatrixController<User>(
loader: _loadUsers,
itemDecoder: User.fromJson,
metaParser: ConfigMetaParser(MetaConfig.nestedMeta),
searchDebounceDelay: Duration(milliseconds: 500), // Custom delay
);
Filtering #
// Single filter
controller.updateFilter('status', 'active');
// Multiple filters
controller.updateFilters({
'status': 'active',
'role': 'admin',
'department': 'engineering',
});
// Remove filter
controller.removeFilter('status');
// Clear all filters
controller.clearFilters();
Sorting #
// Sort by field
controller.updateSorting('name', sortDesc: false);
// Sort descending
controller.updateSorting('created_at', sortDesc: true);
// Clear sorting
controller.clearSorting();
Custom Error Handling #
PaginatrixListView<User>(
controller: controller,
itemBuilder: (context, user, index) => UserTile(user: user),
errorBuilder: (context, error, onRetry) {
if (error is NetworkError) {
return NetworkErrorWidget(
error: error,
onRetry: onRetry,
);
}
return PaginatrixErrorView(
error: error,
onRetry: onRetry,
);
},
)
Custom Empty State #
PaginatrixListView<User>(
controller: controller,
itemBuilder: (context, user, index) => UserTile(user: user),
emptyBuilder: (context) {
return CustomEmptyState(
icon: Icons.people_outline,
title: 'No users found',
description: 'Get started by adding your first user',
action: ElevatedButton(
onPressed: () => Navigator.push(...),
child: Text('Add User'),
),
);
},
)
Custom Loader #
PaginatrixListView<User>(
controller: controller,
itemBuilder: (context, user, index) => UserTile(user: user),
appendLoaderBuilder: (context) {
return AppendLoader(
customLoader: MyCustomLoader(),
message: 'Loading more users...',
);
},
)
Pull-to-Refresh #
PaginatrixListView<User>(
controller: controller,
itemBuilder: (context, user, index) => UserTile(user: user),
onPullToRefresh: () async {
await controller.refresh();
},
)
Manual Pagination #
// Load specific page
await controller.loadPage(3);
// Load next page
await controller.loadNextPage();
// Load previous page
await controller.loadPreviousPage();
// Refresh current page
await controller.refresh();
State Monitoring #
// Listen to state changes
controller.stream.listen((state) {
if (state.isLoading) {
print('Loading...');
} else if (state.hasError) {
print('Error: ${state.error}');
} else if (state.hasItems) {
print('Items: ${state.items.length}');
}
});
// Check current state
if (controller.state.isLoading) {
// Show loading indicator
}
if (controller.state.hasError) {
// Show error message
}
Request Cancellation #
The controller automatically cancels previous requests when:
- A new request is made
- The controller is disposed
- Search/filter/sort changes
You can also manually cancel:
controller.cancelRequests();
π‘ Examples #
Basic ListView #
class UsersPage extends StatefulWidget {
@override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
late final PaginatrixController<User> _controller;
@override
void initState() {
super.initState();
_controller = PaginatrixController<User>(
loader: _loadUsers,
itemDecoder: User.fromJson,
metaParser: ConfigMetaParser(MetaConfig.nestedMeta),
);
_controller.loadFirstPage();
}
Future<Map<String, dynamic>> _loadUsers({
required int page,
required int perPage,
QueryCriteria? query,
CancelToken? cancelToken,
}) async {
final response = await dio.get(
'/users',
queryParameters: {
'page': page,
'per_page': perPage,
if (query?.searchTerm != null) 'search': query!.searchTerm,
},
cancelToken: cancelToken,
);
return response.data;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: PaginatrixListView<User>(
controller: _controller,
itemBuilder: (context, user, index) {
return ListTile(
leading: CircleAvatar(child: Text(user.name[0])),
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
);
}
@override
void dispose() {
_controller.close();
super.dispose();
}
}
GridView with Search #
class ProductsPage extends StatefulWidget {
@override
_ProductsPageState createState() => _ProductsPageState();
}
class _ProductsPageState extends State<ProductsPage> {
late final PaginatrixController<Product> _controller;
@override
void initState() {
super.initState();
_controller = PaginatrixController<Product>(
loader: _loadProducts,
itemDecoder: Product.fromJson,
metaParser: ConfigMetaParser(MetaConfig.nestedMeta),
);
_controller.loadFirstPage();
}
Future<Map<String, dynamic>> _loadProducts({
required int page,
required int perPage,
QueryCriteria? query,
CancelToken? cancelToken,
}) async {
final response = await dio.get(
'/products',
queryParameters: {
'page': page,
'per_page': perPage,
if (query?.searchTerm != null) 'q': query!.searchTerm,
},
cancelToken: cancelToken,
);
return response.data;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Products'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(60),
child: Padding(
padding: EdgeInsets.all(8),
child: TextField(
decoration: InputDecoration(
hintText: 'Search products...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) {
_controller.updateSearchTerm(value);
},
),
),
),
),
body: PaginatrixGridView<Product>(
controller: _controller,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, product, index) {
return ProductCard(product: product);
},
),
);
}
@override
void dispose() {
_controller.close();
super.dispose();
}
}
BLoC Pattern Integration #
// BLoC
class UsersBloc extends Bloc<UsersEvent, UsersState> {
final PaginatrixCubit<User> _paginationCubit;
UsersBloc() : _paginationCubit = PaginatrixCubit<User>(
loader: _loadUsers,
itemDecoder: User.fromJson,
metaParser: ConfigMetaParser(MetaConfig.nestedMeta),
) {
_paginationCubit.loadFirstPage();
}
@override
UsersState get initialState => UsersState.initial(_paginationCubit.state);
@override
Stream<UsersState> mapEventToState(UsersEvent event) async* {
// Handle events and update pagination
}
@override
Future<void> close() {
_paginationCubit.close();
return super.close();
}
}
// Widget
BlocBuilder<PaginatrixCubit<User>, PaginationState<User>>(
bloc: usersBloc.paginationCubit,
builder: (context, state) {
return PaginatrixListView<User>(
cubit: usersBloc.paginationCubit,
itemBuilder: (context, user, index) => UserTile(user: user),
);
},
)
For more examples, see the Examples Directory.
π API Overview #
PaginatrixController #
Main controller for managing pagination state.
Key Methods:
loadFirstPage()- Load the first pageloadNextPage()- Load the next pageloadPage(int page)- Load a specific pagerefresh()- Refresh the current pageupdateSearchTerm(String term)- Update search term (debounced)updateFilter(String key, dynamic value)- Update a filterupdateFilters(Map<String, dynamic> filters)- Update multiple filtersupdateSorting(String field, {bool sortDesc})- Update sortingretry()- Retry the last failed requestclose()- Dispose the controller
Properties:
state- Current pagination statestream- Stream of state changes
PaginationState #
Represents the current state of pagination.
Properties:
items- List of current itemsstatus- Current status (loading, loaded, error, etc.)meta- Pagination metadataerror- Current error (if any)currentQuery- Current query criteria
Extension Methods:
isLoading- Check if loadinghasError- Check if has errorhasItems- Check if has itemsisEmpty- Check if emptyshouldShowLoading- Should show loading indicatorshouldShowError- Should show error viewshouldShowEmpty- Should show empty view
Meta Parsers #
ConfigMetaParser - Pre-configured parsers:
MetaConfig.nestedMeta-{data: [], meta: {...}}MetaConfig.resultsFormat-{results: [], page, per_page, ...}MetaConfig.pageBased- Page-based formatMetaConfig.offsetBased- Offset-based format
CustomMetaParser - Custom parser for any format:
CustomMetaParser((data) {
return {
'items': data['custom_items'],
'meta': {
'page': data['current_page'],
'hasMore': data['has_next'],
},
};
})
For complete API documentation, see the API Reference.
β Best Practices #
1. Always Dispose Controllers #
@override
void dispose() {
_controller.close(); // Required!
super.dispose();
}
2. Load First Page in initState #
@override
void initState() {
super.initState();
_controller.loadFirstPage(); // Required!
}
3. Match Your API Structure #
Use the correct MetaParser for your API:
// For nested meta format
metaParser: ConfigMetaParser(MetaConfig.nestedMeta),
// For results format
metaParser: ConfigMetaParser(MetaConfig.resultsFormat),
// For custom format
metaParser: CustomMetaParser((data) => {...}),
4. Handle Errors #
Always provide error builders:
PaginatrixListView<User>(
controller: _controller,
itemBuilder: (context, user, index) => UserTile(user: user),
errorBuilder: (context, error, onRetry) {
return PaginatrixErrorView(
error: error,
onRetry: onRetry,
);
},
)
5. Use Appropriate Loader Types #
Choose loader types that match your app's design:
AppendLoader(
loaderType: LoaderType.bouncingDots, // Modern and smooth
// or
loaderType: LoaderType.skeleton, // For content preview
)
6. Optimize Performance #
-
Use
keyBuilderfor stable item keys:PaginatrixListView<User>( controller: _controller, keyBuilder: (user, index) => user.id, // Stable keys itemBuilder: (context, user, index) => UserTile(user: user), ) -
Adjust
prefetchThresholdfor better UX:PaginatrixListView<User>( controller: _controller, prefetchThreshold: 5, // Load next page when 5 items from end itemBuilder: (context, user, index) => UserTile(user: user), )
7. Search vs Filters #
- Search (
updateSearchTerm): Debounced (400ms), for text search - Filters (
updateFilter): Immediate, for structured filters
// Search - debounced
_controller.updateSearchTerm('john');
// Filters - immediate
_controller.updateFilter('status', 'active');
8. Use Type-Safe Decoders #
Always use type-safe decoders:
itemDecoder: User.fromJson, // β
Type-safe
// Not: itemDecoder: (json) => User.fromJson(json), // β Less type-safe
β οΈ Troubleshooting #
Common Issues #
1. Items Not Loading
Problem: List is empty even though API returns data.
Solutions:
- Check that
loadFirstPage()is called ininitState() - Verify
metaParsermatches your API structure - Check that
itemDecodercorrectly parses items - Ensure API response format matches expected structure
2. Infinite Loading
Problem: Loader keeps spinning, never shows items.
Solutions:
- Check API response format matches
metaParserconfiguration - Verify
hasMoreorlastPageis correctly parsed - Check for errors in console/logs
- Ensure
itemDecoderreturns correct type
3. Errors Not Showing
Problem: Errors occur but error view doesn't appear.
Solutions:
- Provide
errorBuilderin widget:PaginatrixListView<User>( controller: _controller, errorBuilder: (context, error, onRetry) { return PaginatrixErrorView(error: error, onRetry: onRetry); }, )
4. Search Not Working
Problem: Search doesn't trigger reload.
Solutions:
- Ensure search term is included in loader function:
loader: ({page, perPage, query, cancelToken}) async { final params = { 'page': page, 'per_page': perPage, if (query?.searchTerm != null) 'search': query!.searchTerm, }; // ... }
5. Filters Not Applied
Problem: Filters don't affect results.
Solutions:
- Include filters in loader function:
loader: ({page, perPage, query, cancelToken}) async { final params = { 'page': page, 'per_page': perPage, ...query?.filters ?? {}, // Include filters }; // ... }
Debugging Tips #
-
Check State:
print('State: ${controller.state}'); print('Items: ${controller.state.items.length}'); print('Status: ${controller.state.status}'); print('Error: ${controller.state.error}'); -
Monitor Stream:
controller.stream.listen((state) { print('State changed: $state'); }); -
Verify API Response:
loader: ({page, perPage, query, cancelToken}) async { final response = await api.getData(...); print('API Response: $response'); // Debug return response.data; }
For more troubleshooting help, see the Troubleshooting Guide or FAQ.
π Documentation #
For detailed guides and advanced usage:
- π Full Documentation - Complete guides and API reference
- π‘ Examples - Working examples for all features
- π§ Troubleshooting - Common issues and solutions
- β FAQ - Frequently asked questions
Quick Links #
- Getting Started - Installation and setup
- Core Concepts - Understanding the architecture
- Advanced Usage - Search, filtering, sorting
- Error Handling - Comprehensive error handling
- API Reference - Detailed API documentation
π€ Contributing #
Contributions are welcome! Please see CONTRIBUTING.md for details.
How to Contribute #
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Reporting Issues #
If you find a bug or have a feature request, please open an issue on GitHub.
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π Acknowledgments #
- Built with β€οΈ for the Flutter community
- Inspired by the need for a simple, flexible pagination solution
- Thanks to all contributors and users
Made with β€οΈ for the Flutter community
For questions, issues, or contributions, visit the GitHub repository.