pagy 1.1.1 copy "pagy: ^1.1.1" to clipboard
pagy: ^1.1.1 copied to clipboard

A Flutter package for easy pagination with API data fetching, customizable UI, shimmer effects, error handling, and smooth scrolling.

Pagy Logo

Pagy

A powerful Flutter package for effortless API pagination with shimmer effects, error handling, and smooth scrolling

Pub Version MIT License Flutter Platform GitHub Stars

GitHubpub.flutter-io.cnReport BugRequest Feature


✨ Features #

Pagy is a plug-and-play pagination solution for Flutter apps that makes implementing paginated lists incredibly easy:

  • Smart API Integration - Query params, body payloads, and custom headers support
  • 🚫 Auto-cancellation - Duplicate API calls automatically cancelled
  • 🎨 Beautiful UI - Built-in shimmer effects, error states, and empty state handling
  • 🔧 Built-in Parsers - Laravel, Django, and 5+ common API formats supported
  • 📊 Advanced Metadata - Progress tracking, page indicators, and load status
  • 🔐 Interceptors - Custom auth tokens, retries, and request modification
  • 🧩 State Management - Works with Bloc, Riverpod, Provider, or standalone
  • 🌗 Theme Support - Automatic light/dark theme adaptation
  • 🏗️ Clean Architecture - Dependency injection friendly
  • Performance - Optimized scrolling with lazy loading

📦 Installation #

Add this to your pubspec.yaml:

dependencies:
  pagy: ^1.0.0

Then run:

flutter pub get

🎯 Quick Start #

Step 1: Initialize Pagy #

In your main.dart, configure Pagy before running your app:

import 'package:flutter/material.dart';
import 'package:pagy/pagy.dart';

void main() {
  // Initialize Pagy with your API configuration
  PagyConfig().initialize(
    baseUrl: "https://api.example.com/",
    pageKey: 'page',          // Your API's page param name
    limitKey: 'limit',        // Your API's limit param name
    enableLogs: true,         // Enable debug logs
    payloadMode: PaginationPayloadMode.queryParams, // or .payload for body
  );

  runApp(const MyApp());
}

Step 2: Create Your Model #

class Product {
  final int id;
  final String name;
  final String image;
  final double price;

  Product({
    required this.id,
    required this.name,
    required this.image,
    required this.price,
  });

  // Factory for JSON parsing
  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'],
      name: json['name'],
      image: json['image'],
      price: json['price'].toDouble(),
    );
  }

  // Empty constructor for shimmer placeholder
  Product.empty()
      : id = 0,
        name = 'Loading...',
        image = '',
        price = 0.0;
}

Step 3: Set Up Controller #

import 'package:flutter/material.dart';
import 'package:pagy/pagy.dart';

class ProductListScreen extends StatefulWidget {
  const ProductListScreen({super.key});

  @override
  State<ProductListScreen> createState() => _ProductListScreenState();
}

class _ProductListScreenState extends State<ProductListScreen> {
  late PagyController<Product> pagyController;

  @override
  void initState() {
    super.initState();
    
    pagyController = PagyController(
      endPoint: "products",
      requestType: PagyApiRequestType.get,
      fromMap: Product.fromJson,
      limit: 20,
      // Use built-in parser for common response structures
      responseParser: PagyParsers.dataWithPagination,
      // Or custom parser:
      // responseParser: (response) => PagyResponseParser(
      //   list: response['data'],
      //   totalPages: response['pagination']['totalPages'],
      // ),
    );

    // Load initial data
    pagyController.loadData();
  }

  @override
  void dispose() {
    pagyController.controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Products')),
      body: PagyListView<Product>(
        controller: pagyController,
        itemSpacing: 10,
        padding: const EdgeInsets.all(16),
        shimmerEffect: true,
        placeholderItemCount: 10,
        placeholderItemModel: Product.empty(),
        // Use itemBuilderWithIndex to access item's position
        itemBuilderWithIndex: (context, product, index) {
          return ProductCard(
            product: product,
            position: index + 1, // Show item number
          );
        },
      ),
    );
  }
}

💡 New in v1.1.1: Use itemBuilderWithIndex to access the item's index for features like numbering, alternating colors, or position-based logic.


🎨 ItemBuilder Options #

Access the item's position in the list:

PagyListView<Product>(
  controller: pagyController,
  itemBuilderWithIndex: (context, product, index) {
    return Card(
      color: index.isEven ? Colors.white : Colors.grey[100],
      child: ListTile(
        leading: CircleAvatar(child: Text('#${index + 1}')),
        title: Text(product.name),
      ),
    );
  },
)

Without Index (Deprecated) #

If you don't need the index, the old signature still works but is deprecated:

PagyListView<Product>(
  controller: pagyController,
  itemBuilder: (context, product) { // ⚠️ Deprecated
    return ProductCard(product: product);
  },
)

🔧 Built-in Response Parsers #

Reduce boilerplate with pre-built parsers for common API response structures:

// For: { "data": [...], "pagination": { "totalPages": 10 } }
responseParser: PagyParsers.dataWithPagination

// For: { "items": [...], "total_pages": 10 }
responseParser: PagyParsers.itemsWithTotal

// For: { "results": [...], "page_count": 10 }
responseParser: PagyParsers.resultsWithCount

// For Laravel: { "data": [...], "last_page": 10 }
responseParser: PagyParsers.laravel

// For Django: { "results": [...], "count": 100 }
responseParser: (response) => PagyParsers.django(response, itemsPerPage: 20)

// Custom key names
responseParser: (response) => PagyParsers.customKey(
  response,
  itemKey: 'users',
  totalKey: 'totalPages',
)

💡 Common Use Cases #

1. Search & Filter #

// Simple search
await pagyController.search('laptop computers');

// Apply filters
await pagyController.applyFilters({
  'category': 'electronics',
  'price_max': 1000,
  'in_stock': true,
});

// Custom query parameters
await pagyController.loadData(
  queryParameter: {
    'sort': 'price_desc',
    'brand': 'Apple',
  },
);

2. Pull-to-Refresh #

RefreshIndicator(
  onRefresh: () async {
    await pagyController.refresh();
  },
  child: PagyListView<Product>(
    controller: pagyController,
    itemBuilder: (context, product) => ProductCard(product: product),
  ),
)

3. POST Requests with Authentication #

pagyController = PagyController(
  endPoint: "private/orders",
  requestType: PagyApiRequestType.post,
  fromMap: Order.fromJson,
  token: "Bearer YOUR_AUTH_TOKEN",
  headers: {
    'X-Custom-Header': 'value',
  },
  payloadData: {
    'user_id': 123,
    'status': 'active',
  },
  responseParser: PagyParsers.dataWithPagination,
);

4. Grid View #

PagyGridView<Product>(
  controller: pagyController,
  crossAxisCount: 2,
  crossAxisSpacing: 12,
  mainAxisSpacing: 12,
  shimmerEffect: true,
  placeholderItemModel: Product.empty(),
  itemBuilder: (context, product) {
    return ProductGridCard(product: product);
  },
)

5. Show Pagination Info in UI #

// Display current page info
Text('Page ${pagyController.metadata.currentPage} of ${pagyController.metadata.totalPages}')

// Progress indicator
LinearProgressIndicator(value: pagyController.metadata.progress)

// Show loading state
if (pagyController.metadata.hasMore)
  TextButton(
    onPressed: pagyController.loadMore,
    child: const Text('Load More'),
  )

6. Error Handling #

PagyObserver<Product>(
  controller: pagyController,
  builder: (context, state) {
    if (state.error != null) {
      return Column(
        children: [
          Text('Error: ${state.error!.message}'),
          if (state.error!.suggestion != null)
            Text('Suggestion: ${state.error!.suggestion}'),
          ElevatedButton(
            onPressed: pagyController.retry,
            child: const Text('Retry'),
          ),
        ],
      );
    }
    return PagyListView<Product>(...);
  },
)

🎨 Customization #

Custom Error Widget #

PagyListView<Product>(
  controller: pagyController,
  errorBuilder: (message, retry) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.error, size: 64, color: Colors.red),
          const SizedBox(height: 16),
          Text(message, textAlign: TextAlign.center),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: retry,
            child: const Text('Try Again'),
          ),
        ],
      ),
    );
  },
  itemBuilder: (context, product) => ProductCard(product: product),
)

Custom Empty State #

PagyListView<Product>(
  controller: pagyController,
  emptyStateRetryBuilder: (retry) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.inbox, size: 64),
          const Text('No products found'),
          TextButton(
            onPressed: retry,
            child: const Text('Refresh'),
          ),
        ],
      ),
    );
  },
  itemBuilder: (context, product) => ProductCard(product: product),
)

🔄 Migration Guide (if upgrading from <1.0.0) #

Version 1.0.0+ introduces better naming while maintaining backward compatibility. Old parameter names still work but are deprecated and will be removed in v2.0.0.

PagyController #

Old (Deprecated) New (Recommended)
responseMapper responseParser
additionalQueryParams query
paginationMode payloadMode

PagyConfig #

Old (Deprecated) New (Recommended)
apiLogs enableLogs
paginationMode payloadMode

ItemBuilder (v1.1.1+) #

Old (Deprecated) New (Recommended)
itemBuilder: (context, item) => ... itemBuilderWithIndex: (context, item, index) => ...

Why? Access to the item's index enables features like:

  • Item numbering ("#1", "#2", etc.)
  • Alternating row colors
  • Position-based styling
  • Analytics tracking by position

Example Migration #

Old (still works, shows deprecation warnings):

PagyController(
  responseMapper: (response) => PagyResponseParser(...),
  additionalQueryParams: {'sort': 'latest'},
  paginationMode: PaginationPayloadMode.queryParams,
);

PagyListView(
  itemBuilder: (context, item) => ItemWidget(item), // No index access
)

New (recommended):

PagyController(
  responseParser: PagyParsers.dataWithPagination,
  query: {'sort': 'latest'},
  payloadMode: PaginationPayloadMode.queryParams,
);

PagyListView(
  itemBuilderWithIndex: (context, item, index) => ItemWidget(
    item: item,
    position: index + 1,
  ),
)

📝 Full Example #

Check out the complete working example in the example directory, which includes:

  • ListView and GridView implementations
  • Search and filtering
  • Custom error/empty states
  • Shimmer loading effects
  • Pull-to-refresh
  • State management with Riverpod

🤝 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/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

📄 License #

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


👨‍💻 Author #

Hayat Khan

Hayat Khan

Flutter Developer & Open Source Contributor

GitHub LinkedIn Email


If you found this package helpful, please give it a ⭐ on GitHub!

Made with ❤️ by Hayat Khan

4
likes
150
points
18
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package for easy pagination with API data fetching, customizable UI, shimmer effects, error handling, and smooth scrolling.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

MIT (license)

Dependencies

dio, flutter, flutter_staggered_grid_view, skeletonizer

More

Packages that depend on pagy