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+ }
0 commit comments