pagination_helper 1.0.4 copy "pagination_helper: ^1.0.4" to clipboard
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 #

Pagination Helper Logo

pub package License: MIT Flutter

A lightweight, state-management-agnostic Flutter package for implementing pagination with minimal boilerplate. Works with ANY state management solution!

📸 Screenshots #

Example 1: Basic Usage #

Code Example 1

Example 2: Advanced Configuration #

Code Example 2

Example 3: State Management Integration #

Code Example 3

✨ 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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License #

This project is licensed under the MIT License - see the LICENSE file for details.

👤 Author #

Munawer

📝 Changelog #

See CHANGELOG.md for detailed release notes.


Made with ❤️ for the Flutter community

2
likes
155
points
25
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A lightweight and reusable Flutter package for implementing pagination with minimal boilerplate. Includes paginated list/grid views and a powerful pagination mixin.

Repository (GitHub)
View/report issues

Topics

#pagination #infinite-scroll #listview #gridview #mixin

License

MIT (license)

Dependencies

flutter

More

Packages that depend on pagination_helper