π Complete Guide: Pagination in Flutter with 4 State Management Approaches
π Introduction
Pagination is a crucial feature in modern mobile applications. When dealing with large datasets, loading all data at once can:
- β Consume excessive memory
- β Slow down your app
- β Provide poor user experience
- β Waste network bandwidth
In this comprehensive guide, we'll implement pagination with infinite scroll and pull-to-refresh using 4 different state management approaches in Flutter:
-
StatefulWidget - Traditional approach using
setState() - GetX - Reactive and lightweight
- Provider - Google's recommended approach
- BLoC - Event-driven architecture for large apps
β¨ What We'll Build
A Flutter app that displays 100 users with:
- π Infinite Scroll: Auto-loads more data when scrolling to bottom
- π Pull-to-Refresh: Swipe down to refresh
- π Manual Refresh Button: Tap to refresh
- β οΈ Error Handling: Retry on failures
- πΎ 10 Users Per Page: Simulates API pagination
π οΈ Project Setup
1. Create Flutter Project
flutter create pagination_demo
cd pagination_demo
2. Add Dependencies
Update your pubspec.yaml:
name: pagination
description: "A Flutter pagination demo with multiple state management approaches"
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
provider: ^6.1.2 # Provider state management
flutter_bloc: ^8.1.6 # BLoC pattern
equatable: ^2.0.7 # Value equality for BLoC
get: ^4.6.6 # GetX state management
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
3. Install Dependencies
flutter pub get
π¦ Common Files (Models & Data)
These files are shared across all implementations.
User Model (lib/models/user_model.dart)
/// User Model Class
/// Represents a user with basic information
class UserModel {
/// Unique identifier for the user
final int id;
/// User's full name
final String name;
/// User's email address
final String email;
/// Constructor for UserModel
UserModel({required this.id, required this.name, required this.email});
/// Factory constructor to create UserModel from JSON
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
/// Converts UserModel to JSON
Map<String, dynamic> toJson() {
return {'id': id, 'name': name, 'email': email};
}
/// Creates a copy with updated values
UserModel copyWith({int? id, String? name, String? email}) {
return UserModel(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
);
}
@override
String toString() => 'UserModel(id: $id, name: $name, email: $email)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserModel &&
other.id == id &&
other.name == name &&
other.email == email;
}
@override
int get hashCode => id.hashCode ^ name.hashCode ^ email.hashCode;
}
Dummy Data Provider (lib/common/dummy_data.dart)
This simulates API responses with network delays.
import '../models/user_model.dart';
/// DummyData Class
/// Simulates API responses with paginated user data
class DummyData {
/// Total users available
static const int totalUsers = 100;
/// Users per page
static const int usersPerPage = 10;
/// Private constructor
DummyData._();
/// Generate dummy users
static List<UserModel> _generateAllUsers() {
return List.generate(totalUsers, (index) {
final userId = index + 1;
return UserModel(
id: userId,
name: 'User ${userId.toString().padLeft(3, '0')}',
email: 'user$userId@example.com',
);
});
}
/// Cached users list
static final List<UserModel> _allUsers = _generateAllUsers();
/// Fetch paginated users (simulates API call)
static Future<List<UserModel>> fetchUsers({
required int page,
int pageSize = usersPerPage,
}) async {
// Simulate network delay (1-2 seconds)
await Future.delayed(Duration(milliseconds: 1000 + (page % 2) * 1000));
final startIndex = (page - 1) * pageSize;
final endIndex = startIndex + pageSize;
if (startIndex >= _allUsers.length) return [];
return _allUsers.sublist(
startIndex,
endIndex > _allUsers.length ? _allUsers.length : endIndex,
);
}
/// Check if more data is available
static bool hasMoreData({required int currentPage, int pageSize = usersPerPage}) {
return currentPage < (totalUsers / pageSize).ceil();
}
/// Refresh data (returns first page)
static Future<List<UserModel>> refreshData({int pageSize = usersPerPage}) async {
await Future.delayed(const Duration(milliseconds: 1500));
return fetchUsers(page: 1, pageSize: pageSize);
}
/// Get all users
static List<UserModel> getAllUsers() => List.from(_allUsers);
}
1οΈβ£ StatefulWidget Implementation
π What is StatefulWidget?
StatefulWidget is Flutter's built-in way to manage state. It uses setState() to trigger UI rebuilds.
Pros:
- β No external packages needed
- β Simple and straightforward
- β Perfect for small widgets
Cons:
- β Can become messy in large apps
- β Logic mixed with UI
- β Harder to test
π Complete Code
lib/screens/stateful_pagination_screen.dart:
import 'package:flutter/material.dart';
import '../common/dummy_data.dart';
import '../models/user_model.dart';
/// StatefulPaginationScreen
/// Demonstrates pagination using StatefulWidget and setState()
class StatefulPaginationScreen extends StatefulWidget {
const StatefulPaginationScreen({super.key});
@override
State<StatefulPaginationScreen> createState() =>
_StatefulPaginationScreenState();
}
class _StatefulPaginationScreenState extends State<StatefulPaginationScreen> {
// ==================== State Variables ====================
List<UserModel> _users = [];
int _currentPage = 1;
bool _isLoading = false;
bool _hasMoreData = true;
bool _hasError = false;
String _errorMessage = '';
final ScrollController _scrollController = ScrollController();
// ==================== Lifecycle Methods ====================
@override
void initState() {
super.initState();
_loadInitialData();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
// ==================== Data Loading Methods ====================
/// Load initial data
Future<void> _loadInitialData() async {
setState(() {
_isLoading = true;
_hasError = false;
_errorMessage = '';
_currentPage = 1;
});
try {
final users = await DummyData.fetchUsers(page: _currentPage);
setState(() {
_users = users;
_hasMoreData = DummyData.hasMoreData(currentPage: _currentPage);
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_hasError = true;
_errorMessage = 'Failed to load data: $e';
});
}
}
/// Load more data (pagination)
Future<void> _loadMoreData() async {
if (_isLoading || !_hasMoreData) return;
setState(() {
_isLoading = true;
_hasError = false;
});
try {
final nextPage = _currentPage + 1;
final newUsers = await DummyData.fetchUsers(page: nextPage);
setState(() {
_currentPage = nextPage;
_users.addAll(newUsers);
_hasMoreData = DummyData.hasMoreData(currentPage: _currentPage);
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_hasError = true;
_errorMessage = 'Failed to load more data: $e';
});
}
}
/// Refresh data
Future<void> _refreshData() async {
setState(() {
_hasError = false;
_errorMessage = '';
});
try {
final users = await DummyData.refreshData();
setState(() {
_users = users;
_currentPage = 1;
_hasMoreData = DummyData.hasMoreData(currentPage: 1);
});
} catch (e) {
setState(() {
_hasError = true;
_errorMessage = 'Failed to refresh data: $e';
});
}
}
/// Scroll listener
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent * 0.8) {
_loadMoreData();
}
}
// ==================== UI Builder Methods ====================
@override
Widget build(BuildContext context) {
return Scaffold(appBar: _buildAppBar(), body: _buildBody());
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
title: const Text('Stateful Pagination'),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _refreshData,
tooltip: 'Refresh',
),
],
);
}
Widget _buildBody() {
return RefreshIndicator(onRefresh: _refreshData, child: _buildContent());
}
Widget _buildContent() {
if (_isLoading && _users.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (_hasError && _users.isEmpty) {
return _buildErrorView();
}
return _buildUserList();
}
Widget _buildErrorView() {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(_errorMessage, textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _loadInitialData,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
),
);
}
Widget _buildUserList() {
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(8.0),
itemCount: _users.length + (_hasMoreData ? 1 : 0),
itemBuilder: (context, index) {
if (index == _users.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
);
}
return _buildUserCard(_users[index]);
},
);
}
Widget _buildUserCard(UserModel user) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: ListTile(
leading: CircleAvatar(
child: Text(user.id.toString(),
style: const TextStyle(fontWeight: FontWeight.bold)),
),
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text(user.email),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
),
);
}
}
π― Key Concepts
- setState(): Triggers UI rebuild
- ScrollController: Detects scroll position for pagination
- RefreshIndicator: Enables pull-to-refresh
- State Variables: All managed within the State class
π Main File for StatefulWidget
import 'package:flutter/material.dart';
import 'screens/stateful_pagination_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'StatefulWidget Pagination',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const StatefulPaginationScreen(),
);
}
}
2οΈβ£ GetX Implementation
π What is GetX?
GetX is a lightweight and powerful state management solution with minimal boilerplate.
Pros:
- β Very simple syntax
- β Minimal boilerplate
- β Built-in routing, dialogs, snackbars
- β
Reactive programming with
.obs
Cons:
- β Learning curve for reactive programming
- β Can be "too magical" for some developers
π Complete Code
Controller (lib/controllers/user_controller.dart)
import 'package:get/get.dart';
import '../common/dummy_data.dart';
import '../models/user_model.dart';
/// UserController for GetX
/// Manages pagination state using reactive variables
class UserController extends GetxController {
// Reactive state variables
final RxList<UserModel> users = <UserModel>[].obs;
final RxInt currentPage = 1.obs;
final RxBool isLoading = false.obs;
final RxBool isLoadingMore = false.obs;
final RxBool hasMoreData = true.obs;
final RxBool hasError = false.obs;
final RxString errorMessage = ''.obs;
// Computed properties
int get userCount => users.length;
bool get isEmpty => users.isEmpty;
@override
void onInit() {
super.onInit();
loadInitialData();
}
/// Load initial data
Future<void> loadInitialData() async {
isLoading.value = true;
hasError.value = false;
errorMessage.value = '';
currentPage.value = 1;
try {
final fetchedUsers = await DummyData.fetchUsers(page: currentPage.value);
users.value = fetchedUsers;
hasMoreData.value = DummyData.hasMoreData(currentPage: currentPage.value);
isLoading.value = false;
} catch (e) {
isLoading.value = false;
hasError.value = true;
errorMessage.value = 'Failed to load data: $e';
Get.snackbar('Error', 'Failed to load data: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
/// Load more data
Future<void> loadMoreData() async {
if (isLoadingMore.value || !hasMoreData.value) return;
isLoadingMore.value = true;
try {
final nextPage = currentPage.value + 1;
final newUsers = await DummyData.fetchUsers(page: nextPage);
currentPage.value = nextPage;
users.addAll(newUsers);
hasMoreData.value = DummyData.hasMoreData(currentPage: currentPage.value);
isLoadingMore.value = false;
} catch (e) {
isLoadingMore.value = false;
Get.snackbar('Error', 'Failed to load more: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
/// Refresh data
Future<void> refreshData() async {
try {
final fetchedUsers = await DummyData.refreshData();
users.value = fetchedUsers;
currentPage.value = 1;
hasMoreData.value = DummyData.hasMoreData(currentPage: 1);
} catch (e) {
Get.snackbar('Error', 'Failed to refresh: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
}
UI Screen (lib/screens/getx_pagination_screen.dart)
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/user_controller.dart';
import '../models/user_model.dart';
/// GetX Pagination Screen
/// Uses Obx() for reactive UI updates
class GetxPaginationScreen extends StatelessWidget {
const GetxPaginationScreen({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(UserController());
return Scaffold(
appBar: _buildAppBar(controller),
body: _buildBody(controller),
);
}
PreferredSizeWidget _buildAppBar(UserController controller) {
return AppBar(
title: const Text('GetX Pagination'),
centerTitle: true,
actions: [
Obx(() {
return IconButton(
icon: const Icon(Icons.refresh),
onPressed:
controller.isLoading.value ? null : () => controller.refreshData(),
tooltip: 'Refresh',
);
}),
],
);
}
Widget _buildBody(UserController controller) {
return RefreshIndicator(
onRefresh: () => controller.refreshData(),
child: Obx(() => _buildContent(controller)),
);
}
Widget _buildContent(UserController controller) {
if (controller.isLoading.value && controller.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (controller.hasError.value && controller.isEmpty) {
return _buildErrorView(controller);
}
return _buildUserList(controller);
}
Widget _buildErrorView(UserController controller) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(controller.errorMessage.value),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => controller.loadInitialData(),
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
);
}
Widget _buildUserList(UserController controller) {
return NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) {
if (scrollInfo.metrics.pixels >=
scrollInfo.metrics.maxScrollExtent * 0.8) {
if (!controller.isLoadingMore.value && controller.hasMoreData.value) {
controller.loadMoreData();
}
}
return false;
},
child: Obx(() {
return ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount:
controller.users.length + (controller.hasMoreData.value ? 1 : 0),
itemBuilder: (context, index) {
if (index == controller.users.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
);
}
return _buildUserCard(controller.users[index]);
},
);
}),
);
}
Widget _buildUserCard(UserModel user) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: ListTile(
leading: CircleAvatar(
child: Text(user.id.toString(),
style: const TextStyle(fontWeight: FontWeight.bold)),
),
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text(user.email),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
),
);
}
}
π― Key Concepts
-
.obs: Makes variables reactive -
Obx(): Rebuilds widget when reactive variables change -
Get.put(): Dependency injection -
Get.snackbar(): Easy snackbar notifications
π Main File for GetX
import 'package:flutter/material.dart';
import 'screens/getx_pagination_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GetX Pagination',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const GetxPaginationScreen(),
);
}
}
Full Provider and BLoC implementations are provided in the complete source code. Due to length, this article focuses on StatefulWidget and GetX. Visit the GitHub repository for complete code of all 4 implementations.
π Comparison & When to Use What
Quick Comparison Table
| Feature | StatefulWidget | GetX | Provider | BLoC |
|---|---|---|---|---|
| Learning Curve | Easy | Easy-Medium | Medium | Hard |
| Boilerplate | Low | Very Low | Medium | High |
| Testability | Medium | Good | Good | Excellent |
| Performance | Good | Good | Good | Excellent |
| Setup Complexity | Low | Low | Medium | High |
| Code Lines | ~350 | ~300 | ~320 | ~450 |
| Best For | Small Widgets | Quick Apps | Production Apps | Enterprise Apps |
| Package Size | 0 KB (Built-in) | 95 KB | 15 KB | 140 KB |
When to Use Each Approach
π΅ Use StatefulWidget When:
- β Building simple widgets with local state
- β You don't want external dependencies
- β State is only needed in one widget
- β Learning Flutter basics
- β Quick prototypes
π’ Use GetX When:
- β You want minimal boilerplate
- β Building apps quickly
- β Need routing, dialogs, and snackbars
- β Prefer reactive programming
- β Small to medium-sized apps
- β Rapid development needed
π΅ Use Provider When:
- β Following Google's recommendations
- β Need good documentation
- β Building production apps
- β Want balance between simplicity and scalability
- β Medium to large-sized apps
- β Team collaboration
π£ Use BLoC When:
- β Building enterprise-level apps
- β Need high testability
- β Multiple developers working on same codebase
- β Predictable state management is critical
- β Large, complex apps
- β Strict separation of concerns
π» Complete Source Code
Project Structure
lib/
βββ models/
β βββ user_model.dart # User data model
βββ common/
β βββ dummy_data.dart # Dummy data provider
βββ screens/
β βββ stateful_pagination_screen.dart # StatefulWidget version
β βββ getx_pagination_screen.dart # GetX version
β βββ provider_pagination_screen.dart # Provider version
β βββ bloc_pagination_screen.dart # BLoC version
βββ controllers/
β βββ user_controller.dart # GetX controller
βββ providers/
β βββ user_provider.dart # Provider class
βββ bloc/
β βββ user_event.dart # BLoC events
β βββ user_state.dart # BLoC states
β βββ user_bloc.dart # BLoC logic
βββ main.dart # Main app with navigation
GitHub Repository
git clone https://github.com/manishmali816026/flutter-pagination-demo
cd flutter-pagination-demo
flutter pub get
flutter run
π― Conclusion
Congratulations! π You now have 4 production-ready pagination implementations in Flutter!
What You Learned:
β Pagination Fundamentals
- Infinite scroll implementation
- Pull-to-refresh functionality
- Network delay simulation
- Error handling patterns
β State Management Mastery
- StatefulWidget with setState()
- GetX with reactive programming
- Provider with ChangeNotifier
- BLoC with event-driven architecture
β Best Practices
- Scroll position detection
- Loading state management
- User experience optimization
- Code organization
Key Takeaways:
- StatefulWidget - Perfect for simple cases, zero dependencies
- GetX - Fastest development time, minimal boilerplate
- Provider - Google-recommended, production-ready
- BLoC - Most structured, enterprise-grade
Choose Based On:
| Your Need | Best Choice |
|---|---|
| Learning Flutter | StatefulWidget |
| MVP/Prototype | GetX |
| Production App | Provider |
| Large Enterprise | BLoC |
| Small Widget | StatefulWidget |
| Medium App | GetX or Provider |
| High Testability | BLoC |
Next Steps:
π₯ Enhance Your Implementation:
- Add search and filter functionality
- Implement data caching with Hive
- Connect to real REST APIs
- Add unit and widget tests
- Implement sorting features
- Add skeleton loading screens
π₯ Advanced Topics:
- Offline-first architecture
- GraphQL pagination
- Firebase Firestore pagination
- Complex filtering systems
- Performance optimization
- Memory management
π Additional Resources
Official Documentation:
Recommended Reading:
- Flutter Architecture Blueprints
- State Management Comparison
- Pagination Best Practices
- Flutter Performance Tips
π Thank You!
Thank you for reading this comprehensive guide! If you found it helpful:
Show Your Support:
- β€οΈ React to this article
- π¬ Comment with your questions or feedback
- π Bookmark for future reference
- π Share with fellow Flutter developers
- β Star the GitHub repository
Join the Discussion:
- Which state management do you prefer?
- Have you used pagination in production?
- What challenges have you faced?
- Any tips to share with the community?
Connect With Me:
- π» GitHub: @manishmali816026
- πΌ LinkedIn: Manish Mali
π’ More Flutter Content
Want more Flutter tutorials? Follow me for:
- State Management Deep Dives
- Flutter Architecture Patterns
- Performance Optimization Tips
- Real-world App Development
- Flutter Tips & Tricks
Happy Coding! Keep Building Amazing Flutter Apps! π
Tags for Discovery:
flutter #dart #statemanagement #pagination #getx #provider #bloc #mobiledev #programming #tutorial #flutterdev #coding #softwaredevelopment #appdevelopment #tech
Article Last Updated: January 2025
Written with β€οΈ for the Flutter Community
Top comments (0)