Pagify

pub package GitHub stars GitHub forks GitHub issues License: MIT

A powerful and flexible Flutter package for implementing paginated lists and grids with built-in loading states, error handling, and optional Advanced network connectivity management.

  • reverse pagination with grid view

Reverse Grid View

  • normal pagination with list view

Normal List View

🚀 Features

  • 🔄 Automatic Pagination: Seamless infinite scrolling with customizable page loading
  • 📱 ListView & GridView Support: Switch between list and grid layouts effortlessly
  • 🌐 Network Connectivity: Built-in network status monitoring and error handling
  • 🎯 Flexible Error Mapping: Custom error handling for Dio and HTTP exceptions
  • ↕️ Reverse Pagination: Support for reverse scrolling (chat-like interfaces)
  • 🎨 Customizable UI: Custom loading, error, and empty state widgets
  • 🎮 Controller Support: Programmatic control over data and scroll position
  • 🔍 Rich Data Operations: Filter, sort, add, remove, and manipulate list data
  • 📊 Status Callbacks: Real-time pagination status updates
  • 🎭 Lottie Animations: Built-in animated loading and error states

📦 Installation

Add this to your package's pubspec.yaml file:

dependencies:
  pagify: ^0.2.3

Then run:

flutter pub get

Dependencies

This package uses the following dependencies:

  • connectivity_plus - for network connectivity checking
  • dio (optional) - for enhanced HTTP error handling
  • http (optional) - for basic HTTP error handling
  • lottie (optional) - for default loading animations

🎯 Quick Start

1. ListView Implementation

import 'package:flutter/material.dart';
import 'package:pagify/pagify.dart';
import 'package:dio/dio.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: PaginatedListExample(),
    );
  }
}

class PaginatedListExample extends StatefulWidget {
  @override
  _PaginatedListExampleState createState() => _PaginatedListExampleState();
}

class _PaginatedListExampleState extends State<PaginatedListExample> {
  late PagifyController<Post> controller;

  @override
  void initState() {
    super.initState();
    controller = PagifyController<Post>();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Paginated Posts')),
      body: Pagify<ApiResponse, Post>.listView(
        controller: controller,
        asyncCall: _fetchPosts,
        mapper: _mapResponse,
        errorMapper: _errorMapper,
        itemBuilder: _buildPostItem,
        onUpdateStatus: (status) {
          print('Pagination status: $status');
        },
      ),
    );
  }

  Future<ApiResponse> _fetchPosts(BuildContext context, int page) async {
    final dio = Dio();
    final response = await dio.get(
      'https://jsonplaceholder.typicode.com/posts',
      queryParameters: {'_page': page, '_limit': 10},
    );
    
    return ApiResponse.fromJson(response.data);
  }

  PagifyData<Post> _mapResponse(ApiResponse response) {
    return PagifyData<Post>(
      data: response.posts,
      paginationData: PaginationData(
        perPage: 10,
        totalPages: response.totalPages,
      ),
    );
  }

  PagifyErrorMapper get _errorMapper => PagifyErrorMapper(
    errorWhenDio: (DioException e) => 'Network error: ${e.message}',
    errorWhenHttp: (HttpException e) => 'HTTP error: ${e.message}',
  );

  Widget _buildPostItem(BuildContext context, List<Post> data, int index, Post post) {
    return ListTile(
      leading: CircleAvatar(child: Text('${post.id}')),
      title: Text(post.title),
      subtitle: Text(post.body, maxLines: 2, overflow: TextOverflow.ellipsis),
    );
  }
}

2. GridView Implementation

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Photo Grid')),
    body: Pagify<PhotoResponse, Photo>.gridView(
      controller: controller,
      crossAxisCount: 2,
      childAspectRatio: 0.8,
      mainAxisSpacing: 8.0,
      crossAxisSpacing: 8.0,
      asyncCall: _fetchPhotos,
      mapper: _mapPhotoResponse,
      errorMapper: _errorMapper,
      itemBuilder: _buildPhotoCard,
    ),
  );
}

Widget _buildPhotoCard(BuildContext context, List<Photo> data, int index, Photo photo) {
  return Card(
    child: Column(
      children: [
        Expanded(
          child: Image.network(
            photo.thumbnailUrl,
            fit: BoxFit.cover,
          ),
        ),
        Padding(
          padding: EdgeInsets.all(8.0),
          child: Text(
            photo.title,
            style: TextStyle(fontSize: 12),
            textAlign: TextAlign.center,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    ),
  );
}

🎮 Controller Usage

The PagifyController provides powerful methods to manipulate your data:

class ControllerExample extends StatefulWidget {
  @override
  _ControllerExampleState createState() => _ControllerExampleState();
}

class _ControllerExampleState extends State<ControllerExample> {
  late PagifyController<Post> controller;

  @override
  void initState() {
    super.initState();
    controller = PagifyController<Post>();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Controller Demo'),
        actions: [
          IconButton(
            icon: Icon(Icons.filter_list),
            onPressed: _filterPosts,
          ),
          IconButton(
            icon: Icon(Icons.sort),
            onPressed: _sortPosts,
          ),
        ],
      ),
      body: Pagify<ApiResponse, Post>.listView(
        controller: controller,
        // ... other properties
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: "add",
            onPressed: _addRandomPost,
            child: Icon(Icons.add),
          ),
          SizedBox(height: 8),
          FloatingActionButton(
            heroTag: "scroll",
            onPressed: () => controller.moveToMaxBottom(),
            child: Icon(Icons.arrow_downward),
          ),
        ],
      ),
    );
  }

  void _filterPosts() {
    controller.filterAndUpdate((post) => post.title.contains('et'));
  }

  void _sortPosts() {
    controller.sort((a, b) => a.title.compareTo(b.title));
  }

  void _addRandomPost() {
    final randomPost = Post(
      id: DateTime.now().millisecondsSinceEpoch,
      title: 'New Post ${DateTime.now()}',
      body: 'This is a dynamically added post',
      userId: 1,
    );
    controller.addItem(randomPost);
  }
}

🔧 Advanced Configuration

Network Connectivity Monitoring

Pagify<ApiResponse, Post>.listView(
  controller: controller,
  listenToNetworkConnectivityChanges: true,
  onConnectivityChanged: (isConnected) {
    if (isConnected) {
      print('Network restored');
    } else {
      print('Network lost');
    }
  },
  noConnectionText: 'Please check your internet connection',
  // ... other properties
)

Custom Loading and Error States

Pagify<ApiResponse, Post>.listView(
  controller: controller,
  loadingBuilder: Container(
    padding: EdgeInsets.all(20),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CircularProgressIndicator(color: Colors.blue),
        SizedBox(height: 16),
        Text('Loading awesome content...'),
      ],
    ),
  ),
  errorBuilder: (PagifyException error) => Container(
    padding: EdgeInsets.all(20),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.error_outline, size: 64, color: Colors.red),
        SizedBox(height: 16),
        Text(error.msg, textAlign: TextAlign.center),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: () => controller.refresh(),
          child: Text('Retry'),
        ),
      ],
    ),
  ),
  emptyListView: Container(
    padding: EdgeInsets.all(20),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.inbox_outlined, size: 64, color: Colors.grey),
        SizedBox(height: 16),
        Text('No posts available'),
      ],
    ),
  ),
  // ... other properties
)

Reverse Pagination (Chat-like)

Pagify<MessageResponse, Message>.listView(
  controller: controller,
  isReverse: true,  // Messages appear from bottom
  asyncCall: _fetchMessages,
  mapper: _mapMessages,
  errorMapper: _errorMapper,
  itemBuilder: _buildMessage,
  onSuccess: (context, data) {
    print('Loaded ${data.length} messages');
  },
)

Status Callbacks

Pagify<ApiResponse, Post>.listView(
  controller: controller,
  onUpdateStatus: (PagifyAsyncCallStatus status) {
    switch (status) {
      case PagifyAsyncCallStatus.loading:
        print('Loading data...');
        break;
      case PagifyAsyncCallStatus.success:
        print('Data loaded successfully');
        break;
      case PagifyAsyncCallStatus.error:
        print('Error occurred');
        break;
      case PagifyAsyncCallStatus.networkError:
        print('Network error');
        break;
      case PagifyAsyncCallStatus.initial:
        print('Initial state');
        break;
    }
  },
  onLoading: () => print('About to start loading'),
  onSuccess: (context, data) => print('Success: ${data.length} items'),
  onError: (context, page, exception) => print('Error on page $page: ${exception.msg}'),
  // ... other properties
)

retry function example (important)

  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Example Usage')),
        body: Pagify<ExampleModel, String>.gridView(
            showNoDataAlert: true,
            onLoading: () => log('loading now ...!'),
            onSuccess: (context, data) => log('the data is ready $data'),
            onError: (context, page, e) async{
            await Future.delayed(const Duration(seconds: 2));
            count++;
            if(count > 3){
              return;
            }
            _controller.retry();
              log('page : $page');
              if(e is PagifyNetworkException){
                log('check your internet connection');

              }else if(e is ApiRequestException){
                log('check your server ${e.msg}');

              }else{
                log('other error ...!');
              }
            },
            controller: _controller,
            asyncCall: (context, page)async => await _fetchData(page),
            mapper: (response) => PagifyData(
                data: response.items,
                paginationData: PaginationData(
                  totalPages: response.totalPages,
                  perPage: 10,
                )
            ),
            itemBuilder: (context, data, index, element) => Center(
                child: AppText(element, fontSize: 20,).paddingSymmetric(vertical: 10)
            )
        )
    );
  }

📱 Controller Methods

Method Description
retry() remake the last request if it failed for example
addItem(E item) Add item to the end of the list
addItemAt(int index, E item) Insert item at specific index
addAtBeginning(E item) Add item at the beginning
removeItem(E item) Remove specific item
removeAt(int index) Remove item at index
removeWhere(bool Function(E) condition) Remove items matching condition
replaceWith(int index, E item) Replace item at index
filter(bool Function(E) condition) Get filtered list (non-destructive)
filterAndUpdate(bool Function(E) condition) Filter and update list
sort(int Function(E, E) compare) Sort list in-place
clear() Remove all items
getRandomItem() Get random item from list
accessElement(int index) Safe access to item at index
moveToMaxBottom() Scroll to bottom with animation
moveToMaxTop() Scroll to top with animation

🔄 Pagination Status

enum PagifyAsyncCallStatus {
  initial,      // Before first request
  loading,      // Request in progress
  success,      // Request completed successfully
  error,        // General error occurred
  networkError, // Network connectivity error
}

🎯 Error Handling

PagifyErrorMapper(
            errorWhenDio: (e) {
              String? msg = '';
              switch (e.type) {
                case DioExceptionType.connectionTimeout:
                  msg = 'Connection timeout. Please try again.';

                case DioExceptionType.receiveTimeout:
                  msg = 'Server response timeout.';

                case DioExceptionType.badResponse:
                  msg = 'Server returned ${e.response?.statusCode}';

                default:
                  msg = e.response?.data.toString();
              }

              return PagifyApiRequestException(
                msg ?? 'network error occur',
                pagifyFailure: RequestFailureData(
                  statusCode: e.response?.statusCode,
                  statusMsg: e.response?.statusMessage,
                ),
              );
            } // if you using Dio

            // errorWhenHttp: (e) => PagifyApiRequestException(), // if you using Http
          ),

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

📄 License

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

⭐ Show Your Support

If this package helped you, please give it a ⭐ on GitHub and like it on pub.flutter-io.cn!


Made ❤️ by Ahmed Emara linkedIn