When a Flutter app works with hundreds or thousands of records, loading everything in a single API request is usually a bad idea. It increases response size, consumes more memory, slows JSON parsing, and makes the first screen load slower.
Pagination solves this problem by loading data in smaller batches. For example, an app can fetch the first 20 products and request another 20 when the user scrolls down.
Two popular approaches are offset pagination and cursor pagination. Although both solve the same basic problem, they work differently and suit different applications.
In this guide, you will learn how cursor and offset pagination work, their advantages and limitations, and how to implement both approaches properly in Flutter.
What Is Pagination in Flutter?
Pagination means dividing a large dataset into smaller sections instead of downloading everything at once.
Suppose an e-commerce database contains 50,000 products. Loading all 50,000 records when the screen opens would waste bandwidth and memory.
Instead, the API could return only 20 products:
First Request
↓
Products 1–20
User scrolls
↓
Second Request
↓
Products 21–40
User scrolls again
↓
Third Request
↓
Products 41–60
As a result, the app loads quickly while additional records are fetched only when required.
Pagination is especially useful for:
- Product catalogs
- Social media feeds
- Notifications
- Chat messages
- Search results
- Orders
- User lists
- Admin dashboards
However, choosing the right pagination method is important.
Cursor Pagination vs Offset Pagination: Quick Comparison
| Feature | Offset Pagination | Cursor Pagination |
|---|---|---|
| Request style | Page or offset | Cursor |
| Example | page=2&limit=20 | cursor=abc&limit=20 |
| Implementation | Simple | Slightly more complex |
| Page numbers | Excellent | Usually not suitable |
| Jump to specific page | Easy | Difficult |
| Infinite scrolling | Good | Excellent |
| Frequently changing data | Less stable | More stable |
| New record handling | Can shift results | Usually more consistent |
| Large datasets | Can become expensive | Often more efficient |
| Admin tables | Excellent | Usually unnecessary |
| Social feeds | Works | Often preferred |
The simplest rule is:
Use offset pagination when users need page numbers. Use cursor pagination when users continuously scroll through frequently changing data.
However, there are exceptions, so let’s understand both methods properly.
What Is Offset Pagination?
Offset pagination tells the server how many records should be skipped before returning the next group.
For example:
GET /products?limit=20&offset=0
returns the first 20 products.
The next request becomes:
GET /products?limit=20&offset=20
Afterward:
GET /products?limit=20&offset=40
Therefore, the server receives two important values:
limit
→ Number of records to return
offset
→ Number of records to skip
Consider this dataset:
1 2 3 4 5 6 7 8 9 10
With:
limit = 5
offset = 0
the result is:
1 2 3 4 5
Next:
limit = 5
offset = 5
returns:
6 7 8 9 10
This approach is simple, which is why many REST APIs use it.
Page-Based Pagination Is Usually Similar to Offset Pagination
Many APIs do not expose an offset parameter directly.
Instead, they use:
?page=1&limit=20
Then:
?page=2&limit=20
The backend can calculate the offset using:
offset = (page - 1) × limit
For example:
Page 1
(1 - 1) × 20
= 0
For page 2:
(2 - 1) × 20
= 20
Meanwhile, page 10 produces:
(10 - 1) × 20
= 180
Therefore:
?page=10&limit=20
may internally work like:
?offset=180&limit=20
The exact implementation depends on the backend, but the idea is the same.
Typical Offset Pagination API Response
An API may return:
{
"data": [
{
"id": 1,
"name": "iPhone"
},
{
"id": 2,
"name": "MacBook"
}
],
"page": 1,
"limit": 20,
"total": 500,
"total_pages": 25
}
Flutter can now determine:
Current Page → 1
Total Pages → 25
Total Records → 500
Because of this, offset pagination works extremely well for interfaces that display numbered pages.
Create an Offset Pagination Model in Flutter
First, create a reusable pagination model:
class OffsetPage<T> {
const OffsetPage({
required this.items,
required this.page,
required this.totalPages,
required this.total,
});
final List<T> items;
final int page;
final int totalPages;
final int total;
bool get hasMore => page < totalPages;
}
Next, create a product model:
class Product {
const Product({
required this.id,
required this.name,
});
final int id;
final String name;
factory Product.fromJson(
Map<String, dynamic> json,
) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
);
}
}
Now both the data and pagination information have clear types.
Implement Offset Pagination with Dio
Your service can accept the current page:
class ProductService {
ProductService(this._dio);
final Dio _dio;
Future<OffsetPage<Product>> getProducts({
required int page,
int limit = 20,
}) async {
final response = await _dio.get(
'/products',
queryParameters: {
'page': page,
'limit': limit,
},
);
final json =
response.data as Map<String, dynamic>;
final rawItems =
json['data'] as List<dynamic>;
final products = rawItems
.map(
(item) => Product.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
return OffsetPage(
items: products,
page: json['page'] as int,
totalPages:
json['total_pages'] as int,
total: json['total'] as int,
);
}
}
The first request uses:
getProducts(page: 1);
Afterward:
getProducts(page: 2);
The Flutter implementation remains straightforward.
Offset Pagination with GetX
For a feature-first Flutter project, you might have:
lib/
└── features/
└── products/
├── controller/
│ └── product_controller.dart
├── model/
│ └── product_model.dart
├── repository/
│ └── product_repository.dart
├── services/
│ └── product_service.dart
├── screens/
│ └── product_screen.dart
└── widgets/
└── product_card.dart
Then your controller could be:
class ProductController extends GetxController {
ProductController(this._service);
final ProductService _service;
final products = <Product>[].obs;
final isLoading = false.obs;
final isLoadingMore = false.obs;
int currentPage = 1;
bool hasMore = true;
Future<void> loadProducts() async {
if (isLoading.value) return;
try {
isLoading.value = true;
final result = await _service.getProducts(
page: 1,
);
products.assignAll(result.items);
currentPage = 1;
hasMore = result.hasMore;
} finally {
isLoading.value = false;
}
}
Future<void> loadMore() async {
if (isLoadingMore.value || !hasMore) {
return;
}
try {
isLoadingMore.value = true;
final nextPage = currentPage + 1;
final result = await _service.getProducts(
page: nextPage,
);
products.addAll(result.items);
currentPage = nextPage;
hasMore = result.hasMore;
} finally {
isLoadingMore.value = false;
}
}
}
Here, isLoadingMore prevents the same page from being requested repeatedly while an existing request is still running.
The Main Problem with Offset Pagination
Offset pagination becomes less reliable when records are frequently inserted or deleted.
For example, imagine this feed:
A
B
C
D
E
F
G
H
I
J
The first request uses:
limit = 5
offset = 0
It returns:
A
B
C
D
E
Before the second request, a new record X is inserted at the beginning.
Now the database contains:
X
A
B
C
D
E
F
G
H
I
J
The second request still uses:
offset = 5
However, record E has moved to position 5.
Consequently, page two could return:
E
F
G
H
I
The user sees E twice.
That is a duplicate caused by the dataset changing between requests.
Offset Pagination Can Also Skip Records
Deletion creates the opposite problem.
Suppose the original dataset is:
A
B
C
D
E
F
G
H
I
J
The user has already loaded:
A B C D E
Now B gets deleted.
The dataset becomes:
A
C
D
E
F
G
H
I
J
When the app requests:
offset = 5
the server starts at G.
As a result, F may be skipped completely.
Therefore, offset pagination can have problems with rapidly changing feeds.
What Is Cursor Pagination?
Cursor pagination approaches the problem differently.
Instead of saying:
Skip 20 records.
the client effectively says:
Continue after the position returned by the previous request.
For example, the first request might be:
GET /products?limit=20
The API returns:
{
"data": [],
"next_cursor": "abc123",
"has_more": true
}
The next request becomes:
GET /products?limit=20&cursor=abc123
Flutter does not need to calculate the next page.
Instead, it simply stores the cursor returned by the backend.
How Cursor Pagination Works
Suppose products are ordered newest first.
The first request returns:
Product 100
Product 99
Product 98
Product 97
Product 96
The backend also sends:
nextCursor = cursor_for_product_96
The next request sends that cursor back.
Then the server continues with:
Product 95
Product 94
Product 93
Product 92
Product 91
The flow becomes:
First Request
↓
100 99 98 97 96
↓
Cursor
↓
Second Request
↓
95 94 93 92 91
Unlike offset pagination, the next request is based on a known position in the ordered dataset.
Why Cursor Pagination Handles Changing Data Better
Consider the same feed:
A
B
C
D
E
F
G
H
The first request returns:
A B C D E
and a cursor that represents the position after E.
Now a new item appears:
X
A
B
C
D
E
F
G
H
The next request does not say:
Skip 5 records.
Instead, it asks the backend to continue after the previous cursor.
Therefore, the server can return:
F
G
H
without treating the newly inserted X as part of the already traversed position.
This behavior makes cursor pagination especially useful for continuously changing datasets.
Where Cursor Pagination Works Best
Cursor pagination is a strong option for:
- Social media feeds
- Notification lists
- Activity feeds
- Chat history
- Transaction timelines
- News feeds
- Infinite scrolling
- Frequently updated datasets
For example, a notification feed may receive new records every few seconds. Since users normally scroll rather than jump to page 37, cursor pagination fits that experience naturally.
Cursor Pagination Requires Stable Ordering
Cursor pagination is not automatically reliable.
The backend needs deterministic ordering.
For example:
ORDER BY created_at DESC
can be problematic if multiple records have the same timestamp.
Imagine:
Product A → 10:00:00
Product B → 10:00:00
Product C → 10:00:00
A cursor containing only the timestamp cannot uniquely identify the position.
Instead, the backend could order by:
created_at DESC,
id DESC
The cursor can then represent both values internally.
Consequently, each pagination position remains deterministic.
Treat Cursors as Opaque Values in Flutter
Flutter generally should not decode or modify a cursor.
Avoid:
final decoded =
decodeCursor(nextCursor);
Instead:
nextCursor = response.nextCursor;
Then send it directly:
queryParameters: {
'limit': 20,
if (nextCursor != null)
'cursor': nextCursor,
}
This separation is important because cursor design belongs to the backend.
If the backend changes its cursor encoding later, your Flutter application does not need to change.
Cursor Pagination Response Model
Create:
class CursorPage<T> {
const CursorPage({
required this.items,
required this.nextCursor,
required this.hasMore,
});
final List<T> items;
final String? nextCursor;
final bool hasMore;
}
Unlike offset pagination, you do not need:
currentPage
totalPages
Instead, the important state is:
nextCursor
hasMore
Implement Cursor Pagination with Dio
class ProductService {
ProductService(this._dio);
final Dio _dio;
Future<CursorPage<Product>> getProducts({
String? cursor,
int limit = 20,
}) async {
final response = await _dio.get(
'/products',
queryParameters: {
'limit': limit,
if (cursor != null)
'cursor': cursor,
},
);
final json =
response.data as Map<String, dynamic>;
final rawItems =
json['data'] as List<dynamic>;
final products = rawItems
.map(
(item) => Product.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
return CursorPage(
items: products,
nextCursor:
json['next_cursor'] as String?,
hasMore:
json['has_more'] as bool? ?? false,
);
}
}
The initial request:
getProducts();
does not contain a cursor.
Later requests use:
getProducts(
cursor: nextCursor,
);
That is the main client-side difference.
Cursor Pagination with GetX
class ProductController extends GetxController {
ProductController(this._service);
final ProductService _service;
final products = <Product>[].obs;
final isLoading = false.obs;
final isLoadingMore = false.obs;
String? nextCursor;
bool hasMore = true;
Future<void> loadProducts() async {
if (isLoading.value) return;
try {
isLoading.value = true;
final result =
await _service.getProducts();
products.assignAll(result.items);
nextCursor = result.nextCursor;
hasMore = result.hasMore;
} finally {
isLoading.value = false;
}
}
Future<void> loadMore() async {
if (isLoadingMore.value || !hasMore) {
return;
}
try {
isLoadingMore.value = true;
final result =
await _service.getProducts(
cursor: nextCursor,
);
products.addAll(result.items);
nextCursor = result.nextCursor;
hasMore = result.hasMore;
} finally {
isLoadingMore.value = false;
}
}
}
Notice how the pagination state changes.
Offset pagination uses:
currentPage++;
Cursor pagination uses:
nextCursor = result.nextCursor;
Implement Infinite Scrolling in Flutter
Both approaches can use the same Flutter scroll logic.
Create:
final scrollController =
ScrollController();
Then register a listener:
@override
void onInit() {
super.onInit();
scrollController.addListener(
_onScroll,
);
}
Next:
void _onScroll() {
if (!scrollController.hasClients) {
return;
}
final position =
scrollController.position;
if (position.pixels >=
position.maxScrollExtent - 300) {
loadMore();
}
}
Using a threshold such as 300 means the next page starts loading before the user reaches the absolute bottom.
As a result, scrolling can feel smoother.
Finally, clean up the controller:
@override
void onClose() {
scrollController.dispose();
super.onClose();
}
Prevent Duplicate Pagination Requests
Scroll events can fire repeatedly.
Without protection, your app could do:
loadMore()
loadMore()
loadMore()
loadMore()
before the first request finishes.
Therefore, always protect the method:
if (isLoadingMore.value || !hasMore) {
return;
}
After that, use finally:
try {
isLoadingMore.value = true;
// Request next page.
} finally {
isLoadingMore.value = false;
}
This simple pattern prevents many pagination bugs.
Protect Against Duplicate Items
Even when the backend implements pagination correctly, defensive deduplication can be valuable.
Create a set of loaded IDs:
final Set<int> loadedIds = {};
Then append only new records:
for (final product in result.items) {
if (loadedIds.add(product.id)) {
products.add(product);
}
}
Set.add() returns true when the value was not already present.
Consequently, duplicate IDs are ignored.
This approach is more efficient than repeatedly scanning the entire product list.
Reset Pagination When Filters Change
Suppose the user loads:
category = shoes
and receives:
cursor = abc123
Then they switch to:
category = shirts
The old cursor belongs to the shoes query.
Therefore, reset:
nextCursor = null;
hasMore = true;
products.clear();
loadedIds.clear();
Afterward, request the shirts dataset from the beginning.
The same rule applies when changing:
- Search text
- Category
- Sort order
- Price range
- Date filter
- Location
- Status filter
Pagination state belongs to the query that created it.
Refreshing Paginated Data
Refreshing and loading more are different operations.
During refresh:
Clear old pagination position
↓
Request first batch
↓
Replace existing data
During load more:
Keep current data
↓
Request next batch
↓
Append new data
For cursor pagination:
Future<void> refreshProducts() async {
nextCursor = null;
hasMore = true;
products.clear();
loadedIds.clear();
await loadProducts();
}
For offset pagination:
Future<void> refreshProducts() async {
currentPage = 1;
hasMore = true;
products.clear();
await loadProducts();
}
This keeps pagination state predictable.
Pagination Loading States
A good Flutter screen should distinguish between initial loading and loading more.
Initial load:
Screen Opens
↓
Full Loader
Pagination:
Existing Product
Existing Product
Existing Product
↓
Small Bottom Loader
Do not replace the entire list with a spinner every time another page is requested.
Flutter ListView Example
Obx(() {
if (controller.isLoading.value &&
controller.products.isEmpty) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
controller:
controller.scrollController,
itemCount:
controller.products.length +
(controller.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index ==
controller.products.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child:
CircularProgressIndicator(),
),
);
}
final product =
controller.products[index];
return ProductCard(
product: product,
);
},
);
})
Existing products remain visible while the next batch loads.
Therefore, the user can continue reading instead of seeing the entire screen disappear.
Handle Pagination Errors Separately
Suppose the first 20 products load successfully.
Then the second request fails.
The app should not replace those 20 valid products with a full error screen.
Instead, show:
Product
Product
Product
Product
Unable to load more.
[ Try Again ]
Initial request failure and pagination failure are different states.
A practical controller can track:
final initialError = ''.obs;
final loadMoreError = ''.obs;
Then:
Initial Error
→ Full-screen error
Load-More Error
→ Keep existing content + retry
This creates a much better user experience.
Offset Pagination and Database Performance
A simplified offset query could look like:
SELECT *
FROM products
ORDER BY created_at DESC
LIMIT 20
OFFSET 10000;
For deep pagination, the database may need to locate and move past many records before returning the requested rows.
Consequently, large offsets can become expensive depending on the database, indexes, query plan, filters, and ordering.
However, this does not mean offset pagination is always slow.
A properly indexed database with reasonable dataset sizes may handle it perfectly well.
Always measure real backend performance before optimizing.
Cursor Pagination and Database Performance
A simplified cursor-style query could look like:
SELECT *
FROM products
WHERE id < :last_id
ORDER BY id DESC
LIMIT 20;
Instead of skipping thousands of rows, the query continues from a known key.
With a suitable index, this approach can work efficiently for deep sequential pagination.
For more complex sorting, the backend may use:
created_at
+
id
as the continuation key.
Again, the exact query depends on the backend and database.
Can Cursor Pagination Go Backward?
Yes, if the backend supports it.
For example:
{
"data": [],
"next_cursor": "next123",
"previous_cursor": "prev456"
}
The app can then navigate in either direction.
However, infinite-scroll applications often only need:
next_cursor
because users continuously move through the dataset in one direction.
Can Cursor Pagination Show Page Numbers?
Not naturally.
Offset pagination easily supports:
1 2 3 4 5 ... 50
because Flutter can request:
?page=37
directly.
Cursor pagination normally follows a sequence of continuation positions.
Therefore, jumping directly to page 37 may not be possible unless the backend provides additional functionality.
This is one of the biggest reasons offset pagination remains useful.
What About Total Record Count?
Offset APIs commonly return:
total = 5000
totalPages = 250
Cursor APIs may return only:
nextCursor
hasMore
For an infinite feed, that is often enough.
The UI usually needs to know:
Is there more content?
rather than:
Are there exactly 43,821 records?
Nevertheless, a cursor-based API can still return a total count if the application needs it.
Offset Pagination for E-Commerce Apps
Offset pagination can be a strong choice for a traditional product catalog.
For example:
GET /products
?page=5
&limit=20
&category=shoes
&sort=price_asc
Users may want:
Page 1
Page 2
Page 3
...
Additionally, filters and search results often work naturally with page-based navigation.
Therefore, cursor pagination is not automatically better simply because the dataset is large.
Cursor Pagination for Social Feeds
Social feeds behave differently.
New content may be created constantly:
New Post
New Post
New Post
Meanwhile, users normally browse by scrolling.
They rarely need:
Go to page 46.
As a result, cursor pagination often matches the product experience better.
Cursor Pagination for Chat Messages
Chat is another strong cursor use case.
Users may have:
Newest Messages
↑
Older Messages
↑
Older Messages
An API could provide:
GET /messages
?before=cursor
&limit=50
When the user scrolls upward, Flutter requests older messages.
Because chat datasets continuously change, cursor-based continuation can be a natural solution.
Offset Pagination for Admin Dashboards
Consider an admin table:
Showing 41–60 of 4,820 students
Previous
1 2 3 4 5 ... 241
Next
Here, users need:
- Page numbers
- Total records
- Direct navigation
- Predictable table pages
Offset pagination matches these requirements very well.
Using cursor pagination here may add complexity without improving the user experience.
Should Flutter Decide Which Pagination Method to Use?
Usually, no.
The choice depends heavily on:
- Backend architecture
- Database
- Indexes
- Dataset size
- Data update frequency
- Sorting requirements
- User experience
- Page-number requirements
- Infinite-scroll requirements
Flutter mainly consumes the pagination contract provided by the backend.
Therefore, frontend and backend developers should agree on the strategy before implementing the feature.
Offset Pagination Advantages
Offset pagination has several important benefits.
First, it is easy to understand and implement.
In addition, users can jump directly to a specific page:
page=50
It also works naturally with numbered pagination.
Therefore, it is particularly useful for:
- Admin panels
- Data tables
- Reports
- Search results
- Traditional e-commerce catalogs
Offset Pagination Disadvantages
The biggest problem appears when records frequently move between requests.
New inserts can create duplicates. Meanwhile, deleted records can cause other records to be skipped.
Deep offsets may also become more expensive depending on the backend query.
For that reason, offset pagination may not be ideal for a large, continuously changing feed.
Cursor Pagination Advantages
Cursor pagination offers more stable continuation through changing datasets.
Moreover, it fits infinite scrolling naturally because Flutter only needs to store the next cursor.
A well-designed cursor query can also perform efficiently for deep sequential pagination.
Therefore, cursor pagination is commonly useful for:
- Social feeds
- Notifications
- Chat
- Activity streams
- Large timelines
Cursor Pagination Disadvantages
Cursor pagination requires more backend planning.
For example, developers need stable ordering and reliable cursor generation.
Additionally, jumping directly to an arbitrary page is difficult.
Traditional numbered pagination is also less natural.
Consequently, cursor pagination is not automatically the best choice for every application.
Which Pagination Should You Choose?
Use offset pagination when:
- Users need page numbers.
- Users need direct page navigation.
- Exact total pages are important.
- The dataset is reasonably stable.
- You are building an admin table or traditional catalog.
- Simple implementation matters.
On the other hand, consider cursor pagination when:
- The UI uses infinite scrolling.
- Records change frequently.
- New records are constantly inserted.
- Deep sequential pagination is common.
- Users do not need arbitrary page jumping.
- You are building feeds, chat, notifications, or timelines.
Practical Decision Flow
Use this simple flow:
Does the UI need page numbers?
│
Yes
↓
Offset Pagination
No
↓
Does data change frequently?
│
Yes
↓
Cursor Pagination
No
↓
Either approach may work
↓
Choose according to
backend + UX requirements
The decision should come from product requirements rather than trends.
Common Flutter Pagination Mistakes
Several mistakes appear frequently in real projects.
First, avoid loading thousands of records just to skip pagination.
Moreover, prevent duplicate loadMore() requests with an isLoadingMore guard.
When search or filters change, reset the current page or cursor before requesting new data.
Likewise, keep existing items visible when a load-more request fails.
Another common mistake is relying only on:
items.length < limit
to detect the final page.
Instead, prefer explicit backend information such as:
has_more
next_cursor
total_pages
when the API provides it.
Finally, use stable item IDs and consider defensive deduplication for infinite feeds.
Performance Tips for Flutter Pagination
Pagination improves network efficiency, but Flutter UI performance still matters.
Use:
ListView.builder()
for long lists because widgets are created as needed.
In addition, optimize:
- Image loading
- Image caching
- Widget rebuilds
- JSON parsing
- API response size
- Item keys
- Server-side search
- Server-side sorting
- Server-side filters
Avoid performing expensive calculations directly inside:
itemBuilder
because that work may repeat during scrolling and rebuilding.
For unusually large JSON pages, profile the app first. If parsing itself causes visible jank, moving CPU-heavy parsing to an isolate may help.
How Many Records Should Each Page Load?
There is no universal perfect page size.
Common values include:
10
20
25
50
100
However, the right value depends on the payload.
For example, 50 small text records may be lightweight. In contrast, 50 deeply nested records with images and additional metadata may be expensive.
Therefore, consider:
- Response size
- Network speed
- Parsing cost
- Memory usage
- Backend performance
- UI design
- User scrolling behavior
Start with a reasonable value such as 20 or 25, then measure actual application performance.
Frequently Asked Questions
What is the main difference between cursor and offset pagination?
Offset pagination continues by skipping a number of records. Cursor pagination continues from a position returned by the previous API response.
Is cursor pagination faster than offset pagination?
Not always.
Cursor-based database queries can perform better for deep sequential pagination when designed with appropriate indexes. However, actual performance depends on the database, query, filters, ordering, and dataset.
Which pagination is better for Flutter?
Neither is universally better.
Offset pagination works well for numbered pages and admin tables. In contrast, cursor pagination often fits infinite feeds and rapidly changing datasets.
Which pagination should I use for infinite scrolling?
Cursor pagination is often an excellent choice.
Nevertheless, offset pagination can also provide smooth infinite scrolling when the dataset is reasonably stable.
Can offset pagination cause duplicate records?
Yes. New records inserted before the current offset can shift previously loaded records into later pages.
Can offset pagination skip records?
Yes. Deletions or ordering changes can shift records before the next request and cause some records to be missed.
Does cursor pagination completely prevent duplicates?
No.
Reliable results still depend on stable ordering and a correct backend implementation. Defensive client-side deduplication can also help.
Should Flutter decode a cursor?
Usually not.
Treat the cursor as an opaque value, store it, and return it to the backend.
Which method is better for an admin dashboard?
Offset pagination is often more suitable because admin dashboards commonly need page numbers, totals, and direct navigation.
Which method is better for social media?
Cursor pagination is often more suitable because feeds change frequently and users normally browse through infinite scrolling.
Which method is better for chat messages?
Cursor pagination is a natural choice for loading older or newer messages around a known position.
Final Thoughts
Both cursor and offset pagination help Flutter applications load large datasets efficiently.
Offset pagination follows this model:
Skip N records
↓
Return next records
It is simple and works especially well when users need numbered pages.
Cursor pagination follows a different model:
Continue from known position
↓
Return next records
Because the continuation point is tied to the ordered dataset, cursor pagination is generally more suitable for feeds that change frequently.
Therefore, think about the actual user experience before choosing an approach.
For a dashboard:
Page 1
Page 2
Page 3
offset pagination may be exactly what you need.
For a social feed:
Scroll
↓
Load More
↓
Scroll
↓
Load More
cursor pagination may be a better fit.
Most importantly, Flutter developers should not choose pagination based only on which method sounds more modern. The best strategy depends on the backend, database, dataset behavior, and how users navigate the data.




