JetPaginator class

Simple, powerful infinite scroll pagination for any API format

JetPaginator provides a unified way to create infinite scroll lists and grids that work with ANY API pagination format. Built on top of the official infinite_scroll_pagination package for maximum reliability and performance.

Key Features

  • Official Package: Built on infinite_scroll_pagination for robust state management
  • Works with ANY API response format
  • Support for offset-based, page-based, cursor-based, and custom pagination
  • Smart Error Handling: Built-in error handling with custom error widgets
  • Automatic error handling and retry functionality with proper error parsing
  • Pull-to-refresh integration with Riverpod invalidate support
  • Optimized builds: Efficient state management through PagingController
  • Customizable loading and error indicators with error context and ref access
  • Separate error handling for initial load vs. pagination errors
  • List and grid layouts

Performance Optimizations

Efficient State Management:

  • Uses official PagingController for robust state management
  • Automatic error handling and retry logic
  • Memory-efficient handling of large lists
  • Built-in animation support for smooth transitions
  • Optimized scroll performance

Usage Examples

DummyJSON API (offset-based):

JetPaginator.list<Product>(
  fetchPage: (pageKey) => api.getProducts(skip: pageKey, limit: 20),
  parseResponse: (response, pageKey) => PageInfo(
    items: (response['products'] as List)
        .map((json) => Product.fromJson(json))
        .toList(),
    nextPageKey: response['skip'] + response['limit'] < response['total']
        ? response['skip'] + response['limit']
        : null,
  ),
  itemBuilder: (product, index) => ProductCard(product: product),
)

With Provider Support (recommended for non-family providers):

// Define a simple provider (not family)
final allProductsProvider = FutureProvider<List<Product>>((ref) async {
  // Fetch all products logic here
  return await api.getAllProducts();
});

// Use with provider integration
JetPaginator.list<Product>(
  fetchPage: (pageKey) => api.getProducts(skip: pageKey, limit: 20),
  parseResponse: (response, pageKey) => PageInfo(
    items: (response['products'] as List)
        .map((json) => Product.fromJson(json))
        .toList(),
    nextPageKey: response['skip'] + response['limit'] < response['total']
        ? response['skip'] + response['limit']
        : null,
  ),
  itemBuilder: (product, index) => ProductCard(product: product),
  provider: allProductsProvider, // Enable Riverpod integration
)

With Family Providers:

// Family providers can't be passed directly, but you can still manually invalidate
JetPaginator.list<Product>(
  fetchPage: (pageKey) => ref.read(productsProvider(pageKey).future),
  parseResponse: (response, pageKey) => PageInfo(...),
  itemBuilder: (product, index) => ProductCard(product: product),
  onRefresh: () => ref.invalidate(productsProvider), // Manual invalidate
)

Cursor-based API:

JetPaginator.list<Post>(
  fetchPage: (cursor) => api.getPosts(cursor: cursor, limit: 20),
  parseResponse: (response, currentCursor) => PageInfo(
    items: response.data.map((json) => Post.fromJson(json)).toList(),
    nextPageKey: response.pagination?.nextCursor,
    isLastPage: !response.pagination?.hasMore,
  ),
  itemBuilder: (post, index) => PostCard(post: post),
)

Custom Refresh Indicator:

JetPaginator.list<Product>(
  fetchPage: (pageKey) => api.getProducts(skip: pageKey, limit: 20),
  parseResponse: (response, pageKey) => PageInfo(...),
  itemBuilder: (product, index) => ProductCard(product: product),
  // Simple color customization
  refreshIndicatorColor: Colors.green,
  refreshIndicatorBackgroundColor: Colors.grey[100],
  refreshIndicatorStrokeWidth: 3.0,
  refreshIndicatorDisplacement: 50.0,
)

Fully Custom Refresh Indicator:

JetPaginator.list<Product>(
  fetchPage: (pageKey) => api.getProducts(skip: pageKey, limit: 20),
  parseResponse: (response, pageKey) => PageInfo(...),
  itemBuilder: (product, index) => ProductCard(product: product),
  refreshIndicatorBuilder: (context, controller) {
    return AnimatedContainer(
      duration: Duration(milliseconds: 300),
      child: Icon(
        Icons.refresh,
        color: controller.state.isLoading ? Colors.blue : Colors.grey,
        size: 24 + (controller.value * 12), // Animated size
      ),
    );
  },
)

Page-based API:

JetPaginator.list<User>(
  fetchPage: (page) => api.getUsers(page: page, size: 15),
  parseResponse: (response, currentPage) => PageInfo(
    items: response.content.map((json) => User.fromJson(json)).toList(),
    nextPageKey: response.hasNext ? (currentPage as int) + 1 : null,
    totalItems: response.totalElements,
  ),
  itemBuilder: (user, index) => UserCard(user: user),
)

Properties

hashCode int
The hash code for this object.
no setterinherited
runtimeType Type
A representation of the runtime type of the object.
no setterinherited

Methods

noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toString() String
A string representation of this object.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited

Static Methods

grid<T, TResponse>({required Future<TResponse> fetchPage(dynamic pageKey), required PageInfo<T> parseResponse(TResponse response, dynamic currentPageKey), required Widget itemBuilder(T item, int index), required int crossAxisCount, dynamic firstPageKey = 0, bool enablePullToRefresh = true, Object? provider, double crossAxisSpacing = 8.0, double mainAxisSpacing = 8.0, double childAspectRatio = 1.0, Widget refreshIndicatorBuilder(BuildContext context, IndicatorController controller)?, Color? refreshIndicatorColor, Color? refreshIndicatorBackgroundColor, double? refreshIndicatorStrokeWidth, double? refreshIndicatorDisplacement, Widget? loadingIndicator, Widget errorIndicator(Object error, WidgetRef ref)?, Widget fetchMoreErrorIndicator(Object error, WidgetRef ref)?, Widget? noItemsIndicator, Widget? noMoreItemsIndicator, EdgeInsets? padding, bool shrinkWrap = false, ScrollPhysics? physics, VoidCallback? onRetry, VoidCallback? onRefresh, VoidCallback? onNoItemsActionTap, String? noItemsTitle, String? noItemsMessage, String? noItemsActionTitle, String? noMoreItemsTitle, String? noMoreItemsMessage, String? noMoreItemsActionTitle, int itemsThresholdToTriggerLoad = 3}) Widget
Creates an infinite scroll grid that works with any API format
list<T, TResponse>({required Future<TResponse> fetchPage(dynamic pageKey), required PageInfo<T> parseResponse(TResponse response, dynamic currentPageKey), required Widget itemBuilder(T item, int index), dynamic firstPageKey = 0, bool enablePullToRefresh = true, Object? provider, Widget refreshIndicatorBuilder(BuildContext context, IndicatorController controller)?, Color? refreshIndicatorColor, Color? refreshIndicatorBackgroundColor, double? refreshIndicatorStrokeWidth, double? refreshIndicatorDisplacement, Widget? loadingIndicator, Widget errorIndicator(Object error, WidgetRef ref)?, Widget fetchMoreErrorIndicator(Object error, WidgetRef ref)?, Widget? noItemsIndicator, Widget? noMoreItemsIndicator, EdgeInsets? padding, bool shrinkWrap = false, ScrollPhysics? physics, VoidCallback? onRetry, VoidCallback? onRefresh, VoidCallback? onNoItemsActionTap, String? noItemsTitle, String? noItemsMessage, String? noItemsActionTitle, String? noMoreItemsTitle, String? noMoreItemsMessage, String? noMoreItemsActionTitle, int itemsThresholdToTriggerLoad = 3}) Widget
Creates an infinite scroll list that works with any API format