Flutter Pagination Using BLoC & REST API
Flutter Pagination Using BLoC and REST API in Flutter (Complete Real-World Guide)
When I started building larger Flutter applications, one of the first major performance problems I faced was loading too much data at once.
Initially everything worked fine during development because the API only had a few records. But once the application started growing, the UI became laggy, memory usage increased, and scrolling performance started dropping badly.
That is where pagination became extremely important.
Almost every modern application uses pagination:
Instagram feed
Amazon products
Netflix movies
Twitter posts
LinkedIn jobs
YouTube videos
Without pagination, applications become slow very quickly.
In this article, we will build a proper production-style pagination system in Flutter using:
REST API
BLoC Architecture
Infinite Scrolling
Repository Pattern
Error Handling
Loading Indicators
Retry Logic
This is not just a demo implementation. I’ll also explain common mistakes developers make while implementing pagination in real-world Flutter apps.
What Exactly Is Pagination?
Pagination simply means loading data in smaller chunks instead of loading everything together.
For example:
Instead of loading:
10,000 products
we load:
first 20 items
then next 20
then next 20 while scrolling
This improves:
performance
memory usage
API efficiency
user experience
Types of Pagination
Before implementing code, it’s important to understand the different pagination approaches.
1. Offset Pagination
This is the most common type.
Example API:
?page=1&limit=20
or
?offset=20&limit=20
This approach is easy to implement and works well for:
blogs
admin panels
small-to-medium applications
Advantages:
simple implementation
beginner-friendly
supported by most REST APIs
Disadvantages:
can become slower with huge datasets
duplicate records may occur during frequent updates
2. Cursor Pagination
Instead of page numbers, cursor pagination uses IDs or tokens.
Example:
?cursor=last_item_id
Used heavily in:
Instagram
Twitter
Facebook
Advantages:
faster for large-scale apps
better consistency
ideal for real-time systems
Disadvantages:
backend implementation becomes more complex
3. Infinite Scroll Pagination
This is the most popular UX approach today.
As users scroll:
new data loads automatically
Used in:
Instagram
TikTok
YouTube
Amazon
This is what we’ll implement in this article.
Why I Prefer BLoC for Pagination
I’ve implemented pagination using multiple approaches:
setState
Provider
Riverpod
BLoC
For larger applications, BLoC usually scales much better because:
API logic stays outside UI
state handling becomes predictable
loading/error states become cleaner
testing becomes easier
Especially when pagination gets complex.
Project Setup
Create a new Flutter project:
flutter create flutter_pagination_bloc
Add Required Packages
Inside pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.6
dio: ^5.7.0
equatable: ^2.0.5
Run:
flutter pub get
Recommended Folder Structure
One mistake many developers make is mixing UI and API logic together.
For scalable apps, keep things separated.
lib/
│
├── blocs/
│ └── products/
│ ├── products_bloc.dart
│ ├── products_event.dart
│ └── products_state.dart
│
├── models/
│ └── product_model.dart
│
├── repositories/
│ └── products_repository.dart
│
├── services/
│ └── api_service.dart
│
├── screens/
│ └── products_screen.dart
│
└── main.dart
This structure becomes extremely useful once the application grows.
Step 1 — Create Product Model
product_model.dart
class ProductModel {
final int id;
final String title;
ProductModel({
required this.id,
required this.title,
});
factory ProductModel.fromJson(
Map<String, dynamic> json) {
return ProductModel(
id: json['id'],
title: json['title'],
);
}
}
Step 2 — Create API Service
I usually keep API-related logic inside a dedicated service layer.
api_service.dart
import 'package:dio/dio.dart';
class ApiService {
final Dio dio = Dio();
Future<Response> getProducts({
required int page,
}) async {
return await dio.get(
'https://jsonplaceholder.typicode.com/posts',
queryParameters: {
'_page': page,
'_limit': 20,
},
);
}
}
Here:
_pagecontrols current page_limitcontrols item count
Step 3 — Repository Layer
The repository acts as the middle layer between:
API
BLoC
This keeps architecture cleaner.
products_repository.dart
import '../models/product_model.dart';
import '../services/api_service.dart';
class ProductsRepository {
final ApiService apiService;
ProductsRepository(this.apiService);
Future<List<ProductModel>> fetchProducts({
required int page,
}) async {
final response =
await apiService.getProducts(page: page);
return (response.data as List)
.map((e) => ProductModel.fromJson(e))
.toList();
}
}
Step 4 — Create Events
products_event.dart
part of 'products_bloc.dart';
abstract class ProductsEvent {}
class FetchProducts extends ProductsEvent {}
Step 5 — Create States
products_state.dart
part of 'products_bloc.dart';
abstract class ProductsState {}
class ProductsInitial extends ProductsState {}
class ProductsLoading extends ProductsState {}
class ProductsLoaded extends ProductsState {
final List<ProductModel> products;
final bool hasReachedMax;
ProductsLoaded({
required this.products,
required this.hasReachedMax,
});
}
class ProductsError extends ProductsState {
final String message;
ProductsError(this.message);
}
Step 6 — Create Pagination BLoC
This is the main logic.
products_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../models/product_model.dart';
import '../../repositories/products_repository.dart';
part 'products_event.dart';
part 'products_state.dart';
class ProductsBloc
extends Bloc<ProductsEvent, ProductsState> {
final ProductsRepository repository;
int page = 1;
bool isFetching = false;
ProductsBloc(this.repository)
: super(ProductsInitial()) {
on<FetchProducts>((event, emit) async {
if (isFetching) return;
isFetching = true;
try {
List<ProductModel> oldProducts = [];
if (state is ProductsLoaded) {
oldProducts =
(state as ProductsLoaded).products;
}
final newProducts =
await repository.fetchProducts(
page: page,
);
page++;
final products = [
...oldProducts,
...newProducts,
];
emit(
ProductsLoaded(
products: products,
hasReachedMax:
newProducts.isEmpty,
),
);
} catch (e) {
emit(
ProductsError(
e.toString(),
),
);
}
isFetching = false;
});
}
}
Why isFetching Is Important
During fast scrolling, multiple API calls can trigger together.
Without protection:
duplicate requests happen
UI flickers
pagination breaks
I faced this issue while building an eCommerce application with large product listings.
Adding:
bool isFetching = false;
solved the problem completely.
Step 7 — Create Pagination Screen
products_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ProductsScreen extends StatefulWidget {
const ProductsScreen({super.key});
@override
State<ProductsScreen> createState() =>
_ProductsScreenState();
}
class _ProductsScreenState
extends State<ProductsScreen> {
final ScrollController scrollController =
ScrollController();
@override
void initState() {
super.initState();
context.read<ProductsBloc>()
.add(FetchProducts());
scrollController.addListener(() {
if (scrollController.position.pixels >=
scrollController
.position.maxScrollExtent -
200) {
context.read<ProductsBloc>()
.add(FetchProducts());
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Pagination"),
),
body:
BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
if (state is ProductsLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state is ProductsError) {
return Center(
child: Text(state.message),
);
}
if (state is ProductsLoaded) {
return ListView.builder(
controller: scrollController,
itemCount: state.products.length +
(state.hasReachedMax ? 0 : 1),
itemBuilder: (context, index) {
if (index >=
state.products.length) {
return const Padding(
padding: EdgeInsets.all(20),
child: Center(
child:
CircularProgressIndicator(),
),
);
}
final product =
state.products[index];
return ListTile(
title: Text(product.title),
);
},
);
}
return const SizedBox();
},
),
);
}
}
Step 8 — Initialize Everything
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocProvider(
create: (_) => ProductsBloc(
ProductsRepository(
ApiService(),
),
),
child: const ProductsScreen(),
),
);
}
}
Common Pagination Mistakes
These are issues I’ve personally faced while implementing pagination in production apps.
1. Replacing Old Data
Wrong:
emit(ProductsLoaded(products: newProducts));
This removes previously loaded items.
Correct:
final products = [
...oldProducts,
...newProducts,
];
2. API Calls Triggering Multiple Times
This usually happens because:
scroll listener fires repeatedly
Solution:
use loading guards
use debounce logic
3. Loading Too Much Data
Never fetch:
huge payloads
unnecessary fields
Only load what’s needed.
Performance Tips
Use const Widgets
This reduces rebuilds significantly.
Avoid Heavy Widgets Inside Lists
Large widget trees inside ListView.builder
can hurt scrolling performance.
Use Cached Images
For real apps:
cached_network_image
improves performance a lot.
Real-World Improvements
Production apps usually also include:
pull-to-refresh
shimmer loading
offline caching
retry mechanism
search + pagination
filtering
local cache synchronization
Final Thoughts
Pagination is one of those features that looks simple initially but becomes surprisingly complex in real-world applications.
A proper pagination system should:
avoid duplicate API calls
maintain smooth scrolling
handle errors gracefully
scale efficiently
Using:
BLoC
Repository Pattern
Infinite Scrolling
creates a much cleaner and scalable architecture for Flutter applications.
FAQs
Which pagination type is best?
For most modern apps:
infinite scroll pagination
provides the best user experience.
Why use BLoC for pagination?
Because pagination involves:
loading states
error states
API synchronization
list merging
BLoC handles these scenarios very well.
Can pagination work offline?
Yes.
You can combine:
pagination
local database caching
repository pattern
for offline-first architecture.
Conclusion
In this guide, we implemented:
Flutter pagination
Infinite scrolling
REST API integration
BLoC architecture
Repository pattern
Loading states
Error handling
This structure can now be expanded into:
eCommerce applications
social media apps
enterprise dashboards
inventory systems
large-scale Flutter applications
Comments
Post a Comment