Skip to content

Commit 95a3b24

Browse files
committed
Add pagination to the toolkit
1 parent 7277c51 commit 95a3b24

File tree

5 files changed

+207
-0
lines changed

5 files changed

+207
-0
lines changed

lib/dcc_toolkit.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export 'common/extensions/text_theme.dart';
77
export 'common/result/result.dart';
88
export 'common/type_defs.dart';
99
export 'logger/bolt_logger.dart';
10+
export 'pagination/paginated_scroll_view.dart';
11+
export 'pagination/pagination_interface.dart';
12+
export 'pagination/pagination_mixin.dart';
13+
export 'pagination/pagination_state.dart';
1014
export 'style/style.dart';
1115
export 'test_util/devices_sizes.dart';
1216
export 'test_util/presentation_event_catcher.dart';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import 'package:dcc_toolkit/dcc_toolkit.dart';
2+
import 'package:flutter/material.dart';
3+
4+
/// A widget that displays a list of items with pagination.
5+
///
6+
/// This widget is a [CustomScrollView] that displays a paginated list of items.
7+
/// It uses a [PaginationState] to manage the behavior of the widget.
8+
/// It also uses a [NotificationListener] to listen for scroll events and load more items when the user scrolls to the bottom of the list.
9+
class PaginatedScrollView<T> extends StatelessWidget {
10+
/// Creates a new [PaginatedScrollView].
11+
const PaginatedScrollView({
12+
required this.state,
13+
required this.itemBuilder,
14+
this.onLoadMore,
15+
this.topWidget,
16+
this.bottomWidget,
17+
super.key,
18+
});
19+
20+
/// The state of the pagination.
21+
final PaginationState<T> state;
22+
23+
/// The builder for the items.
24+
final Widget Function(BuildContext, T) itemBuilder;
25+
26+
/// The function to call when the user scrolls to the bottom of the list, to load more items.
27+
final void Function()? onLoadMore;
28+
29+
/// Optional widget to display at the top of the list.
30+
final Widget? topWidget;
31+
32+
/// Optional widget to display at the bottom of the list.
33+
final Widget? bottomWidget;
34+
35+
int get _itemCount => state.items.length;
36+
37+
T _fetchItem(int index) {
38+
return state.items[index];
39+
}
40+
41+
@override
42+
Widget build(BuildContext context) {
43+
return NotificationListener<ScrollNotification>(
44+
onNotification:
45+
onLoadMore != null
46+
? (notification) {
47+
final metrics = notification.metrics;
48+
if (metrics.extentAfter == metrics.minScrollExtent) {
49+
// We don't trigger loading if no next page is available or while we are already fetching more
50+
if (state.hasNextPage && !state.isLoading) {
51+
onLoadMore?.call();
52+
return true;
53+
}
54+
}
55+
return false;
56+
}
57+
: null,
58+
child: CustomScrollView(
59+
physics: const AlwaysScrollableScrollPhysics(),
60+
slivers: [
61+
if (topWidget != null) SliverToBoxAdapter(child: topWidget),
62+
SliverList.builder(
63+
itemCount: _itemCount,
64+
itemBuilder: (context, index) => itemBuilder(context, _fetchItem(index)),
65+
),
66+
if (state.hasNextPage)
67+
const SliverToBoxAdapter(
68+
child: Padding(padding: EdgeInsets.all(Sizes.m), child: Center(child: CircularProgressIndicator())),
69+
),
70+
if (bottomWidget != null) SliverToBoxAdapter(child: bottomWidget),
71+
//Bottom insets to be able to scroll the entire content above the FloatingActionButton
72+
const SliverPadding(padding: Paddings.vertical48),
73+
],
74+
),
75+
);
76+
}
77+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import 'package:dcc_toolkit/pagination/pagination_state.dart';
2+
3+
/// Interface for pagination which is used on your bloc state.
4+
abstract interface class PaginationInterface<T> {
5+
/// The current pagination state.
6+
PaginationState<T> get paginationState;
7+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:dcc_toolkit/pagination/pagination_interface.dart';
2+
import 'package:dcc_toolkit/pagination/pagination_state.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
5+
/// Mixin for pagination.
6+
///
7+
/// This mixin is used to handle pagination in a bloc.
8+
/// It provides a method to fetch the items for a given page and a method to initialize the pagination state.
9+
/// It also provides a method to load the next page.
10+
///
11+
/// Example:
12+
/// ```dart
13+
/// class MyBloc extends Bloc<MyEvent, MyState> with PaginationMixin<MyItem, MyState> {
14+
/// @override
15+
/// Future<List<MyItem>?> fetchPageItems(int page, String? searchQuery) async {
16+
/// // Fetch items logic
17+
/// }
18+
///
19+
/// @override
20+
/// Future<void> initializeState({String? searchQuery}) async {
21+
/// // Initialize state logic
22+
/// }
23+
///
24+
/// @override
25+
/// Future<void> loadNextPage(void Function(PaginationState<T>) emitState) async {
26+
/// // Load next page logic
27+
/// }
28+
/// }
29+
/// ```
30+
mixin PaginationMixin<T, S extends PaginationInterface<T>> on Cubit<S> {
31+
/// Fetches the items for the given page
32+
Future<List<T>?> fetchPageItems({required int page, String? searchQuery});
33+
34+
/// Initializes the pagination state
35+
Future<void> initializeState({String? searchQuery});
36+
37+
/// Loads the next page
38+
Future<void> loadNextPage(void Function(PaginationState<T>) emitState) async {
39+
final paginationState = state.paginationState;
40+
if (paginationState.currentPage < paginationState.lastPage) {
41+
emitState(paginationState.copyWith(isLoading: true));
42+
final nextPage = paginationState.currentPage + 1;
43+
final nextItems = await fetchPageItems(page: nextPage, searchQuery: paginationState.searchQuery);
44+
if (nextItems?.isNotEmpty ?? false) {
45+
emitState(
46+
paginationState.copyWith(
47+
items: [...paginationState.items, ...nextItems!],
48+
currentPage: nextPage,
49+
isLoading: false,
50+
),
51+
);
52+
}
53+
}
54+
}
55+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/// State for pagination.
2+
class PaginationState<T> {
3+
/// Creates a new [PaginationState] with the given values.
4+
PaginationState({
5+
this.items = const [],
6+
this.currentPage = 1,
7+
this.lastPage = 1,
8+
this.isLoading = false,
9+
this.loadingInitialPage = true,
10+
this.hasError = false,
11+
this.total = 0,
12+
this.searchQuery,
13+
});
14+
15+
/// All items fetched so far for the loaded pages.
16+
final List<T> items;
17+
18+
/// The current page in the pagination process.
19+
final int currentPage;
20+
21+
/// The last page in the pagination process.
22+
final int lastPage;
23+
24+
/// Whether the current page is being loaded.
25+
final bool isLoading;
26+
27+
/// Whether the initial page is being loaded.
28+
final bool loadingInitialPage;
29+
30+
/// Whether there is an error loading the current page.
31+
final bool hasError;
32+
33+
/// The total number of items.
34+
final int total;
35+
36+
/// The search query to filter the items.
37+
final String? searchQuery;
38+
39+
/// Checks if there is a next page to load for the current [PaginationState].
40+
bool get hasNextPage => currentPage < lastPage;
41+
42+
/// Copies the current state with the given values.
43+
PaginationState<T> copyWith({
44+
List<T>? items,
45+
int? currentPage,
46+
int? lastPage,
47+
bool? isLoading,
48+
bool? loadingInitialPage,
49+
bool? hasError,
50+
int? total,
51+
String? searchQuery,
52+
}) {
53+
return PaginationState<T>(
54+
items: items ?? this.items,
55+
currentPage: currentPage ?? this.currentPage,
56+
lastPage: lastPage ?? this.lastPage,
57+
isLoading: isLoading ?? this.isLoading,
58+
loadingInitialPage: loadingInitialPage ?? this.loadingInitialPage,
59+
hasError: hasError ?? this.hasError,
60+
total: total ?? this.total,
61+
searchQuery: searchQuery ?? this.searchQuery,
62+
);
63+
}
64+
}

0 commit comments

Comments
 (0)