endless_scroll_pagination 0.1.0-beta.1 copy "endless_scroll_pagination: ^0.1.0-beta.1" to clipboard
endless_scroll_pagination: ^0.1.0-beta.1 copied to clipboard

A powerful Flutter package for endless scrolling with cursor-based pagination, pull-to-refresh, and customizable loading states.

example/lib/main.dart

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Infinite Scroll Pagination Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Scroll Examples'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildExampleCard(
            context,
            'Basic Posts Feed',
            'Simple infinite scrolling list like social media',
            Icons.article,
            () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const PostsFeedExample()),
            ),
          ),
          const SizedBox(height: 12),
          _buildExampleCard(
            context,
            'Photo Grid',
            'Grid layout with infinite scrolling',
            Icons.photo_library,
            () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const PhotoGridExample()),
            ),
          ),
          const SizedBox(height: 12),
          _buildExampleCard(
            context,
            'Error Handling',
            'Example with network errors and retry',
            Icons.error_outline,
            () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const ErrorHandlingExample()),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildExampleCard(
    BuildContext context,
    String title,
    String subtitle,
    IconData icon,
    VoidCallback onTap,
  ) {
    return Card(
      child: ListTile(
        leading: Icon(icon, size: 32),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text(subtitle),
        trailing: const Icon(Icons.arrow_forward_ios),
        onTap: onTap,
      ),
    );
  }
}

// Mock Post model
class Post {
  final String id;
  final String title;
  final String content;
  final String author;
  final DateTime createdAt;
  final int likes;

  Post({
    required this.id,
    required this.title,
    required this.content,
    required this.author,
    required this.createdAt,
    required this.likes,
  });
}

// Basic Posts Feed Example
class PostsFeedExample extends StatefulWidget {
  const PostsFeedExample({super.key});

  @override
  State<PostsFeedExample> createState() => _PostsFeedExampleState();
}

class _PostsFeedExampleState extends State<PostsFeedExample> {
  late PaginationController<Post> _controller;

  @override
  void initState() {
    super.initState();
    _controller = PaginationController<Post>(
      loadMore: _loadMorePosts,
      cursorExtractor: (post) => post.id,
      config: const PaginationConfig(pageSize: 10, enablePullToRefresh: true),
    );
  }

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

  Future<PaginationResult<Post>> _loadMorePosts(String? cursor) async {
    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 1500));

    final startIndex = cursor != null ? int.parse(cursor) : 0;
    final posts = List.generate(10, (index) {
      final id = (startIndex + index).toString();
      return Post(
        id: id,
        title: 'Amazing Post #${startIndex + index + 1}',
        content: 'This is the content of post ${startIndex + index + 1}. '
            'It contains some interesting information that users would love to read!',
        author: 'User ${(startIndex + index) % 5 + 1}',
        createdAt: DateTime.now().subtract(Duration(hours: startIndex + index)),
        likes: (startIndex + index) * 3 + 12,
      );
    });

    return PaginationResult<Post>(
      items: posts,
      nextCursor: (startIndex + 10).toString(),
      hasMore: startIndex < 90, // Stop at 100 posts
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Posts Feed'),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: () {
              // Add a new post at the top
              _controller.addItem(
                Post(
                  id: 'new_${DateTime.now().millisecondsSinceEpoch}',
                  title: 'New Post Added!',
                  content: 'This post was added in real-time.',
                  author: 'You',
                  createdAt: DateTime.now(),
                  likes: 0,
                ),
              );
            },
          ),
        ],
      ),
      body: EndlessScrollView<Post>(
        controller: _controller,
        itemBuilder: (context, post, index) {
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      CircleAvatar(
                        backgroundColor: Colors.blue,
                        child: Text(
                          post.author[0],
                          style: const TextStyle(color: Colors.white),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              post.author,
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            Text(
                              _formatTime(post.createdAt),
                              style: TextStyle(
                                color: Colors.grey[600],
                                fontSize: 12,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 12),
                  Text(
                    post.title,
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(post.content),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Icon(
                        Icons.favorite_border,
                        size: 20,
                        color: Colors.grey[600],
                      ),
                      const SizedBox(width: 4),
                      Text('${post.likes}'),
                      const SizedBox(width: 16),
                      Icon(
                        Icons.comment_outlined,
                        size: 20,
                        color: Colors.grey[600],
                      ),
                      const SizedBox(width: 4),
                      const Text('12'),
                      const SizedBox(width: 16),
                      Icon(
                        Icons.share_outlined,
                        size: 20,
                        color: Colors.grey[600],
                      ),
                    ],
                  ),
                ],
              ),
            ),
          );
        },
        emptyBuilder: (context) {
          return const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.article_outlined, size: 64, color: Colors.grey),
                SizedBox(height: 16),
                Text(
                  'No posts yet',
                  style: TextStyle(fontSize: 18, color: Colors.grey),
                ),
                Text(
                  'Pull down to refresh',
                  style: TextStyle(color: Colors.grey),
                ),
              ],
            ),
          );
        },
      ),
    );
  }

  String _formatTime(DateTime dateTime) {
    final now = DateTime.now();
    final difference = now.difference(dateTime);

    if (difference.inMinutes < 1) {
      return 'Just now';
    } else if (difference.inHours < 1) {
      return '${difference.inMinutes}m ago';
    } else if (difference.inDays < 1) {
      return '${difference.inHours}h ago';
    } else {
      return '${difference.inDays}d ago';
    }
  }
}

// Photo Grid Example
class PhotoGridExample extends StatefulWidget {
  const PhotoGridExample({super.key});

  @override
  State<PhotoGridExample> createState() => _PhotoGridExampleState();
}

class _PhotoGridExampleState extends State<PhotoGridExample> {
  late PaginationController<Photo> _controller;

  @override
  void initState() {
    super.initState();
    _controller = PaginationController<Photo>(
      loadMore: _loadMorePhotos,
      cursorExtractor: (photo) => photo.id,
      config: const PaginationConfig(
        pageSize: 20,
        enableInvisibleItemsThreshold: true,
        invisibleItemsThreshold: 5,
      ),
    );
  }

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

  Future<PaginationResult<Photo>> _loadMorePhotos(String? cursor) async {
    await Future.delayed(const Duration(milliseconds: 1000));

    final startIndex = cursor != null ? int.parse(cursor) : 0;
    final photos = List.generate(20, (index) {
      final id = (startIndex + index).toString();
      return Photo(
        id: id,
        url: 'https://picsum.photos/300/300?random=${startIndex + index}',
        title: 'Photo ${startIndex + index + 1}',
      );
    });

    return PaginationResult<Photo>(
      items: photos,
      nextCursor: (startIndex + 20).toString(),
      hasMore: startIndex < 200,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Photo Grid')),
      body: EndlessScrollView<Photo>(
        controller: _controller,
        itemBuilder: (context, photo, index) {
          // Create a grid layout manually
          if (index % 2 == 0) {
            // Even index: create a row with two photos
            final nextPhoto = index + 1 < _controller.state.items.length
                ? _controller.state.items[index + 1]
                : null;

            return Padding(
              padding: const EdgeInsets.all(4.0),
              child: Row(
                children: [
                  Expanded(child: _buildPhotoCard(photo)),
                  const SizedBox(width: 4),
                  Expanded(
                    child: nextPhoto != null
                        ? _buildPhotoCard(nextPhoto)
                        : const SizedBox(),
                  ),
                ],
              ),
            );
          } else {
            // Odd index: already handled in the previous even index
            return const SizedBox.shrink();
          }
        },
        loadingBuilder: (context) {
          return const Padding(
            padding: EdgeInsets.all(20.0),
            child: Center(
              child: Column(
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 8),
                  Text('Loading more photos...'),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  Widget _buildPhotoCard(Photo photo) {
    return Card(
      child: Column(
        children: [
          AspectRatio(
            aspectRatio: 1,
            child: Container(
              decoration: BoxDecoration(
                borderRadius: const BorderRadius.vertical(
                  top: Radius.circular(4),
                ),
                color: Colors.grey[300],
              ),
              child: const Center(
                child: Icon(Icons.image, size: 50, color: Colors.grey),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              photo.title,
              style: const TextStyle(fontSize: 12),
              textAlign: TextAlign.center,
            ),
          ),
        ],
      ),
    );
  }
}

class Photo {
  final String id;
  final String url;
  final String title;

  Photo({required this.id, required this.url, required this.title});
}

// Error Handling Example
class ErrorHandlingExample extends StatefulWidget {
  const ErrorHandlingExample({super.key});

  @override
  State<ErrorHandlingExample> createState() => _ErrorHandlingExampleState();
}

class _ErrorHandlingExampleState extends State<ErrorHandlingExample> {
  late PaginationController<Post> _controller;
  int _loadCount = 0;

  @override
  void initState() {
    super.initState();
    _controller = PaginationController<Post>(
      loadMore: _loadWithErrors,
      cursorExtractor: (post) => post.id,
      config: const PaginationConfig(
        pageSize: 5,
        maxRetries: 3,
        retryDelay: Duration(seconds: 1),
      ),
    );
  }

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

  Future<PaginationResult<Post>> _loadWithErrors(String? cursor) async {
    _loadCount++;

    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 1000));

    // Simulate errors for demonstration
    if (_loadCount == 2 || _loadCount == 5) {
      throw Exception('Network error occurred');
    }

    final startIndex = cursor != null ? int.parse(cursor) : 0;
    final posts = List.generate(5, (index) {
      final id = (startIndex + index).toString();
      return Post(
        id: id,
        title: 'Post #${startIndex + index + 1}',
        content: 'Content for post ${startIndex + index + 1}',
        author: 'Author ${(startIndex + index) % 3 + 1}',
        createdAt: DateTime.now(),
        likes: (startIndex + index) * 2 + 5,
      );
    });

    return PaginationResult<Post>(
      items: posts,
      nextCursor: (startIndex + 5).toString(),
      hasMore: startIndex < 30,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Error Handling'),
        actions: [
          IconButton(
            icon: const Icon(Icons.info_outline),
            onPressed: () {
              showDialog(
                context: context,
                builder: (context) => AlertDialog(
                  title: const Text('Demo Info'),
                  content: const Text(
                    'This demo simulates network errors on the 2nd and 5th load attempts. '
                    'Try scrolling to trigger loading and see how errors are handled.',
                  ),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('OK'),
                    ),
                  ],
                ),
              );
            },
          ),
        ],
      ),
      body: EndlessScrollView<Post>(
        controller: _controller,
        itemBuilder: (context, post, index) {
          return ListTile(
            leading: CircleAvatar(child: Text((index + 1).toString())),
            title: Text(post.title),
            subtitle: Text('By ${post.author} • ${post.likes} likes'),
          );
        },
        loadingBuilder: (context) {
          return const Padding(
            padding: EdgeInsets.all(16.0),
            child: Center(
              child: Column(
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 8),
                  Text('Loading posts...'),
                ],
              ),
            ),
          );
        },
        errorBuilder: (context, error, retry) {
          return Container(
            margin: const EdgeInsets.all(16),
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.red[50],
              border: Border.all(color: Colors.red[200]!),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              children: [
                Icon(Icons.error_outline, color: Colors.red[700], size: 32),
                const SizedBox(height: 8),
                Text(
                  'Failed to load posts',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.red[700],
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  error,
                  style: TextStyle(color: Colors.red[600]),
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 12),
                ElevatedButton.icon(
                  onPressed: retry,
                  icon: const Icon(Icons.refresh),
                  label: const Text('Try Again'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red[700],
                    foregroundColor: Colors.white,
                  ),
                ),
              ],
            ),
          );
        },
        emptyBuilder: (context) {
          return const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.inbox_outlined, size: 64, color: Colors.grey),
                SizedBox(height: 16),
                Text(
                  'No posts available',
                  style: TextStyle(fontSize: 18, color: Colors.grey),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}
0
likes
0
points
27
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful Flutter package for endless scrolling with cursor-based pagination, pull-to-refresh, and customizable loading states.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on endless_scroll_pagination