easy_scroll_pagination 1.1.0
easy_scroll_pagination: ^1.1.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.0.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),
);
},
)
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: CircularProgressIndicator(),
onLoadingMore: Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),
),
onError: (error) => Text('Error: $error'),
onEmpty: 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 itemsstatus- initial | loading | success | failurehasMore- whether more data is availableerrorMessage- error details for failuresnextCursor- cursor for cursor-based pagination
Helpers:
isInitialLoadinghasErrorisEmpty
Use With Provider #
class UsersController extends OffsetPaginationController<User> {
UsersController() : super(fetcher: fetchUsers);
}
ChangeNotifierProvider(
create: (_) => UsersController(),
)
Consumer<UsersController>(
builder: (_, controller, __) {
return PaginatedListView<User>(
controller: controller,
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/.
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.