Skip to main content

easy_scroll_pagination 1.2.0 copy "easy_scroll_pagination: ^1.2.0" to clipboard
easy_scroll_pagination: ^1.2.0 copied to clipboard

A flexible Flutter pagination controller supporting offset and cursor-based pagination with refresh, parallel API calls, and clean state management.

easy_scroll_pagination #

Lightweight pagination controllers and a Flutter list widget for offset, cursor, and parallel pagination. The core controllers are framework-agnostic (ChangeNotifier-based) and easy to integrate with Provider, Riverpod, or Bloc.

Features #

  • Flexible layouts: ListView, GridView, PageView, Column, Row, and Custom
  • Offset pagination (page + limit)
  • Cursor pagination (nextCursor)
  • Parallel offset pagination (merge multiple sources in one list)
  • Infinite scroll widget with pull-to-refresh
  • Configurable scroll threshold
  • Custom widgets for loading, error, and empty states
  • Prevents duplicate fetches
  • Safe disposal handling
  • Clear pagination state model

Installation #

Add to pubspec.yaml:

dependencies:
  easy_scroll_pagination: ^1.2.0

Then run:

flutter pub get

Quick Start #

Offset Pagination (Page + Limit) #

Use for REST APIs or classic page-based endpoints.

Fetcher:

Future<List<User>> fetchUsers(int page, int limit) async {
  final response = await api.get('/users', query: {
    'page': page,
    'limit': limit,
  });

  return (response.data as List)
      .map((e) => User.fromJson(e))
      .toList();
}

Controller:

final usersController = OffsetPaginationController<User>(
  fetcher: fetchUsers,
  limit: 20,
);

UI (Standard List):

PaginatedView<User>.list(
  controller: usersController,
  itemBuilder: (context, user, index) {
    return ListTile(
      title: Text(user.name),
      subtitle: Text(user.email),
    );
  },
)

Add Item Separators #

Use separatorWidget to render spacing/dividers between list items.

PaginatedView<User>.list(
  controller: usersController,
  separatorWidget: const Divider(height: 1),
  itemBuilder: (context, user, index) {
    return ListTile(title: Text(user.name));
  },
)

The separator renders only between loaded items (not after the last item and not after the load-more indicator).

Grid, PageView, and Custom Layouts #

PaginatedView supports various layouts out of the box.

GridView

PaginatedView<User>.grid(
  controller: controller,
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
  itemBuilder: (context, user, index) => Card(child: Text(user.name)),
)

PageView

PaginatedView<User>.page(
  controller: controller,
  itemBuilder: (context, user, index) => Center(child: Text(user.name)),
)

Column/Row (Custom Layouts)

Use .layout() when you need to use non-scrollable widgets like Column or Row inside a SingleChildScrollView, or other custom layouts like StaggeredGrid.

SingleChildScrollView(
  controller: myScrollController,
  child: PaginatedView<User>.layout(
    controller: controller,
    scrollController: myScrollController,
    itemBuilder: (context, user, index) => ListTile(title: Text(user.name)),
    layoutBuilder: (children) => Column(children: children),
  ),
)

Custom Loading and Error UI #

You can customize the look of various states:

PaginatedView<User>.list(
  controller: controller,
  itemBuilder: itemBuilder,
  onInitialLoading: const Center(child: CircularProgressIndicator()),
  onLoadingMore: Padding(
    padding: const EdgeInsets.all(8.0),
    child: const Center(child: CircularProgressIndicator()),
  ),
  onError: const Text('Something went wrong'),
  onEmpty: const Text('No items found'),
)

Cursor Pagination #

Use for Firebase, GraphQL, infinite feeds, or cursor-based APIs.

Response shape (example):

{ "data": [...], "nextCursor": "abc123" }

Fetcher:

Future<CursorResult<Post>> fetchPosts(String? cursor) async {
  final response = await api.get('/posts', query: {
    'cursor': cursor,
  });

  return CursorResult<Post>(
    items: (response.data['data'] as List)
        .map((e) => Post.fromJson(e))
        .toList(),
    nextCursor: response.data['nextCursor'],
    hasMore: response.data['nextCursor'] != null,
  );
}

Controller:

final postsController = CursorPaginationController<Post>(
  fetcher: fetchPosts,
);

UI (Standard List):

PaginatedView<Post>.list(
  controller: postsController,
  itemBuilder: (context, post, index) {
    return ListTile(
      title: Text(post.title),
      subtitle: Text(post.body),
    );
  },
)

Parallel Offset Pagination #

Use when you need to merge multiple paginated sources into one list.

final controller = ParallelOffsetPaginationController<Item>(
  fetchers: [
    (page, limit) => apiA.fetchItems(page, limit),
    (page, limit) => apiB.fetchItems(page, limit),
  ],
  limit: 20,
);

Refresh and Manual Controls #

controller.fetchNext();              // Load next page
controller.fetchNext(refresh: true); // Refresh from start
controller.reset();                  // Clear all data

The PaginatedListView already wires pull-to-refresh to fetchNext(refresh: true).

Lifecycle #

Controllers are ChangeNotifiers. Dispose them when no longer used:

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

Pagination State #

controller.state exposes:

  • items - currently loaded items
  • status - initial | loading | success | failure
  • hasMore - whether more data is available
  • errorMessage - error details for failures
  • nextCursor - cursor for cursor-based pagination

Helpers:

  • isInitialLoading
  • hasError
  • isEmpty

Use With Provider #

class UsersController extends OffsetPaginationController<User> {
  UsersController() : super(fetcher: fetchUsers);
}
ChangeNotifierProvider(
  create: (_) => UsersController(),
)
Consumer<UsersController>(
  builder: (_, controller, __) {
    return PaginatedListView<User>(
      controller: controller,
      separatorWidget: const Divider(height: 1),
      itemBuilder: ...,
    );
  },
)

Architecture Overview #

lib/
  easy_scroll_pagination.dart
  src/
    core/
      pagination_controller.dart
      offset_pagination_controller.dart
      cursor_pagination_controller.dart
      parallel_offset_pagination_controller.dart
      pagination_state.dart
      pagination_status.dart
    flutter/
      paginated_view.dart
      paginated_list_view.dart

Example #

A runnable demo is available in example/.

Publish to pub.dev #

Before publishing, run this release checklist:

  1. Update version and metadata in pubspec.yaml.
  2. Add release notes in CHANGELOG.md.
  3. Validate formatting and lints.
  4. Validate package scoring and publish checks.
flutter format lib example
flutter analyze
flutter pub publish --dry-run

Testing #

test('loads next page correctly', () async {
  final controller = OffsetPaginationController<int>(
    fetcher: (page, limit) async => [1, 2, 3],
  );

  await controller.fetchNext();
  expect(controller.state.items.length, 3);
});

License #

MIT License. See LICENSE.

3
likes
160
points
36
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A flexible Flutter pagination controller supporting offset and cursor-based pagination with refresh, parallel API calls, and clean state management.

Repository (GitHub)
View/report issues

Topics

#pagination #infinite-scroll #flutter #listview #cursor-pagination

License

MIT (license)

Dependencies

cupertino_icons, flutter

More

Packages that depend on easy_scroll_pagination