pagination_helper 1.0.4
pagination_helper: ^1.0.4 copied to clipboard
A lightweight and reusable Flutter package for implementing pagination with minimal boilerplate. Includes paginated list/grid views and a powerful pagination mixin.
π¦ Flutter Pagination Helper #
A lightweight, state-management-agnostic Flutter package for implementing pagination with minimal boilerplate. Works with ANY state management solution!
πΈ Screenshots #
Example 1: Basic Usage #
Example 2: Advanced Configuration #
Example 3: State Management Integration #
β¨ Features #
- π Universal Compatibility - Works with Cubit, Bloc, Provider, Riverpod, GetX, setState, and more
- π PaginatedListView - Automatic infinite scrolling with pull-to-refresh
- π PaginatedGridView - Grid layout with pagination support
- π§© PaginationMixin - Zero framework dependencies
- π Flexible - Offset, page, and cursor-based pagination
- π¨ Customizable - Loading indicators, empty states, thresholds, separators
- π‘οΈ Type-Safe - Fully generic implementation
π₯ Installation #
dependencies:
pagination_helper: ^latest_version
flutter pub get
import 'package:pagination_helper/pagination_helper.dart';
π Quick Start #
1. Add the Widget #
PaginatedListView<Product>(
items: products,
isLoadingMore: isLoadingMore,
onLoadMore: () => cubit.loadMore(),
onRefresh: () => cubit.refresh(), // Optional
itemBuilder: (context, product, index) {
return ProductCard(product: product);
},
)
2. Use PaginationMixin #
class ProductCubit extends Cubit<ProductState> with PaginationMixin {
Future<void> loadMore() async {
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
return await apiService.getProducts(skip: offset, limit: limit);
},
mergeData: (current, newData) => current.copyWith(
products: [...current.products, ...newData.products],
total: newData.total,
),
getCurrentCount: (data) => data.products.length,
getTotalCount: (data) => data.total,
updateState: (isLoading, data, error) {
emit(state.copyWith(
data: data ?? state.data,
isLoadingMore: isLoading,
error: error,
));
},
currentData: state.data,
isCurrentlyLoading: state.isLoadingMore,
);
}
}
That's it! Your pagination is working. π
π Usage Examples #
Grid View #
PaginatedGridView<Product>(
items: products,
isLoadingMore: isLoadingMore,
onLoadMore: () => controller.loadMore(),
crossAxisCount: 2,
itemBuilder: (context, product, index) => ProductGridCard(product),
)
Custom Loading Widget #
PaginatedListView<Product>(
items: products,
isLoadingMore: isLoadingMore,
onLoadMore: () => cubit.loadMore(),
loadingWidget: const Center(
child: CircularProgressIndicator(),
),
itemBuilder: (context, product, index) => ProductCard(product),
)
Custom Empty State #
PaginatedListView<Product>(
items: products,
isLoadingMore: isLoadingMore,
onLoadMore: () => cubit.loadMore(),
emptyWidget: const Center(
child: Text('No products found'),
),
itemBuilder: (context, product, index) => ProductCard(product),
)
π― State Management Examples #
Flutter Bloc/Cubit #
class ProductCubit extends Cubit<ProductState> with PaginationMixin {
Future<void> loadMore() async {
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
return await apiService.getProducts(skip: offset, limit: limit);
},
mergeData: (current, newData) => current.copyWith(
products: [...current.products, ...newData.products],
total: newData.total,
),
getCurrentCount: (data) => data.products.length,
getTotalCount: (data) => data.total,
updateState: (isLoading, data, error) {
emit(state.copyWith(
data: data ?? state.data,
isLoadingMore: isLoading,
error: error,
));
},
currentData: state.data,
isCurrentlyLoading: state.isLoadingMore,
);
}
}
Provider/ChangeNotifier #
class ProductProvider with ChangeNotifier, PaginationMixin {
ProductData _data = ProductData.empty();
bool _isLoadingMore = false;
Future<void> loadMore() async {
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
return await apiService.getProducts(skip: offset, limit: limit);
},
mergeData: (current, newData) => current.copyWith(
products: [...current.products, ...newData.products],
total: newData.total,
),
getCurrentCount: (data) => data.products.length,
getTotalCount: (data) => data.total,
updateState: (isLoading, data, error) {
_isLoadingMore = isLoading;
if (data != null) _data = data;
notifyListeners();
},
currentData: _data,
isCurrentlyLoading: _isLoadingMore,
);
}
}
Riverpod #
class ProductNotifier extends StateNotifier<ProductState>
with PaginationMixin {
Future<void> loadMore() async {
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
return await apiService.getProducts(skip: offset, limit: limit);
},
mergeData: (current, newData) => current.copyWith(
products: [...current.products, ...newData.products],
total: newData.total,
),
getCurrentCount: (data) => data.products.length,
getTotalCount: (data) => data.total,
updateState: (isLoading, data, error) {
state = state.copyWith(
isLoadingMore: isLoading,
data: data ?? state.data,
error: error,
);
},
currentData: state.data,
isCurrentlyLoading: state.isLoadingMore,
);
}
}
GetX #
class ProductController extends GetxController with PaginationMixin {
final products = <Product>[].obs;
final isLoadingMore = false.obs;
Future<void> loadMore() async {
final currentData = ProductData(
products: products.toList(),
total: total.value,
);
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
return await apiService.getProducts(skip: offset, limit: limit);
},
mergeData: (current, newData) => ProductData(
products: [...current.products, ...newData.products],
total: newData.total,
),
getCurrentCount: (data) => data.products.length,
getTotalCount: (data) => data.total,
updateState: (isLoading, data, err) {
isLoadingMore.value = isLoading;
if (data != null) {
products.value = data.products;
total.value = data.total;
}
},
currentData: currentData,
isCurrentlyLoading: isLoadingMore.value,
);
}
}
setState (StatefulWidget) #
class _ProductListPageState extends State<ProductListPage>
with PaginationMixin {
List<Product> products = [];
bool isLoadingMore = false;
Future<void> loadMore() async {
final currentData = ProductData(products: products, total: total);
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
return await apiService.getProducts(skip: offset, limit: limit);
},
mergeData: (current, newData) => ProductData(
products: [...current.products, ...newData.products],
total: newData.total,
),
getCurrentCount: (data) => data.products.length,
getTotalCount: (data) => data.total,
updateState: (isLoading, data, err) {
setState(() {
isLoadingMore = isLoading;
if (data != null) {
products = data.products;
total = data.total;
}
});
},
currentData: currentData,
isCurrentlyLoading: isLoadingMore,
);
}
}
π Advanced Features #
Pagination Types #
Offset-Based (Default)
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
// offset: 0, 10, 20, 30...
return await api.getProducts(skip: offset, limit: limit);
},
// ... other parameters
);
Page-Based
await loadMoreWithPage<ProductData>(
fetchData: (page, limit) async {
// page: 1, 2, 3, 4...
return await api.getProducts(page: page, limit: limit);
},
// ... other parameters
);
Cursor-Based
await loadMoreWithCursor<ProductData>(
fetchData: (cursor, limit) async {
// cursor: null, "cursor1", "cursor2"...
return await api.getProducts(cursor: cursor, limit: limit);
},
getNextCursor: (data) => data.nextCursor,
hasMoreData: (data) => data.nextCursor != null,
// ... other parameters
);
Error Handling #
await loadMoreData<ProductData>(
fetchData: (offset, limit) async {
try {
return await api.getProducts(skip: offset, limit: limit);
} catch (e) {
throw Exception('Failed to load: $e');
}
},
updateState: (isLoading, data, error) {
if (error != null) {
// Handle error
emit(state.copyWith(error: error));
} else if (data != null) {
// Handle success
emit(state.copyWith(data: data));
}
emit(state.copyWith(isLoadingMore: isLoading));
},
// ... other parameters
);
Customization Options #
PaginatedListView<Product>(
items: products,
isLoadingMore: isLoadingMore,
onLoadMore: () => cubit.loadMore(),
loadMoreThreshold: 500.0, // Trigger 500px before bottom
separatorBuilder: (context, index) => const Divider(),
enableRefresh: true, // Enable/disable pull-to-refresh
itemBuilder: (context, product, index) => ProductCard(product),
)
π Common Patterns #
Data Model #
class ProductData {
final List<Product> products;
final int total;
ProductData({required this.products, required this.total});
ProductData copyWith({
List<Product>? products,
int? total,
}) {
return ProductData(
products: products ?? this.products,
total: total ?? this.total,
);
}
static ProductData empty() => ProductData(products: [], total: 0);
}
Loading Initial Data #
@override
void initState() {
super.initState();
loadMore(); // Load first page
}
// Or in Cubit constructor
ProductCubit({required this.apiService})
: super(ProductState.initial()) {
loadMore();
}
π API Reference #
PaginatedListView<T> #
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
items |
List<T> |
β | - | List of items to display |
isLoadingMore |
bool |
β | - | Loading flag from state |
itemBuilder |
Widget Function(BuildContext, T, int) |
β | - | Builder for items |
onLoadMore |
VoidCallback |
β | - | Called when more items needed |
onRefresh |
Future<void> Function()? |
β | null |
Pull-to-refresh callback |
loadingWidget |
Widget? |
β | Default | Custom loading indicator |
emptyWidget |
Widget? |
β | null |
Widget shown when empty |
loadMoreThreshold |
double |
β | 200.0 |
Distance from bottom (px) |
separatorBuilder |
Widget Function(BuildContext, int)? |
β | null |
Item separators |
enableRefresh |
bool |
β | true |
Enable pull-to-refresh |
PaginatedGridView<T> #
Inherits all parameters from PaginatedListView plus:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
crossAxisCount |
int |
β | - | Number of columns |
childAspectRatio |
double |
β | 1.0 |
Width/height ratio |
crossAxisSpacing |
double |
β | 0.0 |
Horizontal spacing |
mainAxisSpacing |
double |
β | 0.0 |
Vertical spacing |
PaginationMixin #
loadMoreData<TData>
Offset-based pagination method.
| Parameter | Type | Required | Description |
|---|---|---|---|
fetchData |
Future<TData> Function(int offset, int limit) |
β | Fetch function |
mergeData |
TData Function(TData current, TData newData) |
β | Merge current and new data |
getCurrentCount |
int Function(TData) |
β | Get current item count |
getTotalCount |
int Function(TData) |
β | Get total available count |
updateState |
void Function(bool isLoading, TData? data, String? error) |
β | Update state callback |
currentData |
TData |
β | Current data from state |
isCurrentlyLoading |
bool |
β | Current loading flag |
limit |
int |
β | Items per page (default: 10) |
onError |
void Function(dynamic)? |
β | Optional error callback |
loadMoreWithPage<TData>
Page-based pagination (page starts from 1). Same parameters as loadMoreData, but fetchData receives (page, limit).
loadMoreWithCursor<TData>
Cursor-based pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
fetchData |
Future<TData> Function(String? cursor, int limit) |
β | Fetch with cursor |
getNextCursor |
String? Function(TData) |
β | Extract next cursor |
hasMoreData |
bool Function(TData) |
β | Check if more available |
mergeData |
TData Function(TData current, TData newData) |
β | Merge data |
updateState |
void Function(bool isLoading, TData? data, String? error) |
β | Update state |
currentData |
TData |
β | Current data |
isCurrentlyLoading |
bool |
β | Loading flag |
limit |
int |
β | Items per page (default: 10) |
β οΈ Requirements #
- Flutter:
>=3.0.0 - Dart:
>=3.0.0 <4.0.0
π€ Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
- 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
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π€ Author #
Munawer
- GitHub: @munawerdev
- Repository: pagination_helper
π Changelog #
See CHANGELOG.md for detailed release notes.
Made with β€οΈ for the Flutter community