Skip to content

Commit 395ce01

Browse files
committed
- Implemented generic pagination for all list pages
- Removed celebrity search page
1 parent 1dc3fea commit 395ce01

File tree

12 files changed

+265
-771
lines changed

12 files changed

+265
-771
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
4+
/// A generic pagination state class for handling paginated data with infinite scroll
5+
abstract class PaginationConsumerState<T, W extends ConsumerStatefulWidget>
6+
extends ConsumerState<W> {
7+
late ScrollController scrollController;
8+
int currentPage = 1;
9+
final List<T> items = [];
10+
bool isLoadingMore = false;
11+
bool hasMore = true;
12+
bool isInitialLoading = true;
13+
14+
// Customizable parameters
15+
int threshold = 200; // Scroll threshold to trigger next page load
16+
int initialPage = 1; // Initial page number
17+
Axis scrollDirection = Axis.vertical; // Scroll direction
18+
bool reverse = false; // Whether to reverse the scroll direction
19+
20+
@override
21+
void initState() {
22+
super.initState();
23+
currentPage = initialPage;
24+
scrollController = ScrollController();
25+
loadPage(currentPage);
26+
27+
scrollController.addListener(_scrollListener);
28+
}
29+
30+
void _scrollListener() {
31+
if (scrollController.position.pixels >=
32+
scrollController.position.maxScrollExtent - threshold &&
33+
!isLoadingMore &&
34+
hasMore) {
35+
loadPage(++currentPage);
36+
}
37+
}
38+
39+
@override
40+
void dispose() {
41+
scrollController.removeListener(_scrollListener);
42+
scrollController.dispose();
43+
super.dispose();
44+
}
45+
46+
/// Load a specific page of data
47+
Future<void> loadPage(int page) async {
48+
if (page == initialPage) {
49+
setState(() {
50+
isInitialLoading = true;
51+
});
52+
} else {
53+
setState(() {
54+
isLoadingMore = true;
55+
});
56+
}
57+
58+
try {
59+
final result = await fetchData(page);
60+
61+
setState(() {
62+
if (result.isNotEmpty) {
63+
if (page == initialPage) {
64+
items.clear(); // Clear items only when refreshing first page
65+
}
66+
items.addAll(result);
67+
} else {
68+
hasMore = false;
69+
}
70+
isLoadingMore = false;
71+
isInitialLoading = false;
72+
});
73+
} catch (e) {
74+
setState(() {
75+
isLoadingMore = false;
76+
isInitialLoading = false;
77+
hasMore = false; // Stop trying to load more on error
78+
});
79+
}
80+
}
81+
82+
/// Refresh the data from the first page
83+
Future<void> refresh() async {
84+
currentPage = initialPage;
85+
hasMore = true;
86+
await loadPage(currentPage);
87+
}
88+
89+
/// Abstract method to fetch data - must be implemented by subclasses
90+
Future<List<T>> fetchData(int page);
91+
92+
/// Build the content with customizable options
93+
Widget buildContent({
94+
required BuildContext context,
95+
required Widget Function(T item) itemBuilder,
96+
ScrollPhysics? physics,
97+
EdgeInsets? padding,
98+
int crossAxisCount = 2,
99+
double childAspectRatio = 2 / 3,
100+
double crossAxisSpacing = 8,
101+
double mainAxisSpacing = 8,
102+
bool addAutomaticKeepAlives = true,
103+
bool addRepaintBoundaries = true,
104+
bool addSemanticIndexes = true,
105+
}) {
106+
if (isInitialLoading) {
107+
return const Center(child: CircularProgressIndicator());
108+
}
109+
110+
return Column(
111+
children: [
112+
Expanded(
113+
child: GridView.builder(
114+
controller: scrollController,
115+
scrollDirection: scrollDirection,
116+
reverse: reverse,
117+
padding: padding ?? const EdgeInsets.all(8),
118+
physics: physics,
119+
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
120+
crossAxisCount: crossAxisCount,
121+
childAspectRatio: childAspectRatio,
122+
crossAxisSpacing: crossAxisSpacing,
123+
mainAxisSpacing: mainAxisSpacing,
124+
),
125+
itemCount: items.length + (hasMore && isLoadingMore ? 1 : 0),
126+
itemBuilder: (context, index) {
127+
if (index == items.length && hasMore && isLoadingMore) {
128+
return const Center(child: CircularProgressIndicator());
129+
}
130+
return itemBuilder(items[index]);
131+
},
132+
addAutomaticKeepAlives: addAutomaticKeepAlives,
133+
addRepaintBoundaries: addRepaintBoundaries,
134+
addSemanticIndexes: addSemanticIndexes,
135+
),
136+
),
137+
if (hasMore && isLoadingMore && !isInitialLoading)
138+
const Padding(
139+
padding: EdgeInsets.only(bottom: 24.0, top: 8.0),
140+
child: CircularProgressIndicator(),
141+
),
142+
],
143+
);
144+
}
145+
146+
/// Build a list view instead of grid view
147+
Widget buildListContent({
148+
required BuildContext context,
149+
required Widget Function(T item) itemBuilder,
150+
ScrollPhysics? physics,
151+
EdgeInsets? padding,
152+
bool addAutomaticKeepAlives = true,
153+
bool addRepaintBoundaries = true,
154+
bool addSemanticIndexes = true,
155+
}) {
156+
if (isInitialLoading) {
157+
return const Center(child: CircularProgressIndicator());
158+
}
159+
160+
return Column(
161+
children: [
162+
Expanded(
163+
child: ListView.builder(
164+
controller: scrollController,
165+
scrollDirection: scrollDirection,
166+
reverse: reverse,
167+
padding: padding ?? const EdgeInsets.all(8),
168+
physics: physics,
169+
itemCount: items.length + (hasMore && isLoadingMore ? 1 : 0),
170+
itemBuilder: (context, index) {
171+
if (index == items.length && hasMore && isLoadingMore) {
172+
return const Center(child: CircularProgressIndicator());
173+
}
174+
return itemBuilder(items[index]);
175+
},
176+
addAutomaticKeepAlives: addAutomaticKeepAlives,
177+
addRepaintBoundaries: addRepaintBoundaries,
178+
addSemanticIndexes: addSemanticIndexes,
179+
),
180+
),
181+
if (hasMore && isLoadingMore && !isInitialLoading)
182+
const Padding(
183+
padding: EdgeInsets.only(bottom: 24.0, top: 8.0),
184+
child: CircularProgressIndicator(),
185+
),
186+
],
187+
);
188+
}
189+
}

lib/features/celebrity/presentation/pages/celebrity_search_page.dart

Lines changed: 0 additions & 107 deletions
This file was deleted.
Lines changed: 8 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_movie_clean_architecture/core/utils/pagination_consumer_state.dart';
23
import 'package:flutter_movie_clean_architecture/features/celebrity/domain/entities/person.dart';
34
import 'package:flutter_movie_clean_architecture/features/celebrity/presentation/providers/celebrity_provider.dart';
45
import 'package:flutter_movie_clean_architecture/features/celebrity/presentation/widgets/person_card.dart';
@@ -11,84 +12,17 @@ class PopularPersonsPage extends ConsumerStatefulWidget {
1112
ConsumerState<PopularPersonsPage> createState() => _PopularPersonsPageState();
1213
}
1314

14-
class _PopularPersonsPageState extends ConsumerState<PopularPersonsPage> {
15-
final _scrollController = ScrollController();
16-
int _currentPage = 1;
17-
final List<Person> _persons = [];
18-
bool _isLoadingMore = false;
19-
bool _hasMore = true;
20-
bool _isInitialLoading = true;
21-
15+
class _PopularPersonsPageState extends PaginationConsumerState<Person, PopularPersonsPage> {
2216
@override
23-
void initState() {
24-
super.initState();
25-
_loadPage(_currentPage);
26-
27-
_scrollController.addListener(() {
28-
if (_scrollController.position.pixels >=
29-
_scrollController.position.maxScrollExtent - 200 &&
30-
!_isLoadingMore &&
31-
_hasMore) {
32-
_loadPage(++_currentPage);
33-
}
34-
});
35-
}
36-
37-
Future<void> _loadPage(int page) async {
38-
setState(() {
39-
_isLoadingMore = true;
40-
});
41-
42-
final result = await ref.read(popularPersonsProvider(page).future);
43-
44-
setState(() {
45-
if (result.isNotEmpty) {
46-
_persons.addAll(result);
47-
} else {
48-
_hasMore = false;
49-
}
50-
_isLoadingMore = false;
51-
_isInitialLoading = false;
52-
});
17+
Future<List<Person>> fetchData(int page) async {
18+
return ref.read(popularPersonsProvider(page).future);
5319
}
5420

5521
@override
5622
Widget build(BuildContext context) {
57-
return _isInitialLoading
58-
? const Center(child: CircularProgressIndicator())
59-
: Column(
60-
children: [
61-
Expanded(
62-
child: GridView.builder(
63-
controller: _scrollController,
64-
padding: const EdgeInsets.all(8),
65-
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
66-
crossAxisCount: 2,
67-
childAspectRatio: 2 / 3,
68-
crossAxisSpacing: 8,
69-
mainAxisSpacing: 8,
70-
),
71-
itemCount: _persons.length + (_hasMore && _isLoadingMore ? 1 : 0),
72-
itemBuilder: (context, index) {
73-
if (index == _persons.length) {
74-
return const Center(child: CircularProgressIndicator());
75-
}
76-
return PersonCardWidget(person: _persons[index]);
77-
},
78-
),
79-
),
80-
if (_hasMore && _isLoadingMore && !_isInitialLoading)
81-
const Padding(
82-
padding: EdgeInsets.only(bottom: 24.0, top: 8.0),
83-
child: CircularProgressIndicator(),
84-
),
85-
],
86-
);
87-
}
88-
89-
@override
90-
void dispose() {
91-
_scrollController.dispose();
92-
super.dispose();
23+
return buildContent(
24+
context: context,
25+
itemBuilder: (person) => PersonCardWidget(person: person),
26+
);
9327
}
9428
}

0 commit comments

Comments
 (0)