From bc18c863610f358de62b1b22bda316968904bdaa Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 13 Oct 2024 16:23:29 +0200 Subject: [PATCH 01/37] chore: upgrade to dart 3 --- lib/src/core/paging_controller.dart | 4 +- lib/src/utils/appended_sliver_grid.dart | 4 +- lib/src/utils/listenable_listener.dart | 4 +- .../first_page_error_indicator.dart | 4 +- .../first_page_exception_indicator.dart | 4 +- .../first_page_progress_indicator.dart | 2 +- .../footer_tile.dart | 4 +- .../new_page_error_indicator.dart | 4 +- .../new_page_progress_indicator.dart | 4 +- .../no_items_found_indicator.dart | 2 +- .../widgets/helpers/paged_layout_builder.dart | 7 +- .../layouts/paged_aligned_grid_view.dart | 112 ++++++------------ lib/src/widgets/layouts/paged_grid_view.dart | 38 ++---- lib/src/widgets/layouts/paged_list_view.dart | 75 ++++-------- .../layouts/paged_masonry_grid_view.dart | 112 ++++++------------ lib/src/widgets/layouts/paged_page_view.dart | 4 +- .../layouts/paged_sliver_aligned_grid.dart | 14 +-- .../widgets/layouts/paged_sliver_grid.dart | 4 +- .../widgets/layouts/paged_sliver_list.dart | 10 +- .../layouts/paged_sliver_masonry_grid.dart | 20 +--- pubspec.yaml | 4 +- 21 files changed, 151 insertions(+), 285 deletions(-) diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index bf26707..228f9ad 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -34,10 +34,10 @@ class PagingController /// /// [firstPageKey] is the key to be used in case of a [refresh]. PagingController.fromValue( - PagingState value, { + super.value, { required this.firstPageKey, this.invisibleItemsThreshold, - }) : super(value); + }); ObserverList? _statusListeners = ObserverList(); diff --git a/lib/src/utils/appended_sliver_grid.dart b/lib/src/utils/appended_sliver_grid.dart index c06e103..3eb65fc 100644 --- a/lib/src/utils/appended_sliver_grid.dart +++ b/lib/src/utils/appended_sliver_grid.dart @@ -18,8 +18,8 @@ class AppendedSliverGrid extends StatelessWidget { this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, - }) : super(key: key); + super.key, + }); final IndexedWidgetBuilder itemBuilder; final int itemCount; diff --git a/lib/src/utils/listenable_listener.dart b/lib/src/utils/listenable_listener.dart index c1ba5c5..2ff6224 100644 --- a/lib/src/utils/listenable_listener.dart +++ b/lib/src/utils/listenable_listener.dart @@ -6,8 +6,8 @@ class ListenableListener extends StatefulWidget { required this.listenable, required this.child, this.listener, - Key? key, - }) : super(key: key); + super.key, + }); /// The [Listenable] to which this widget is listening. /// diff --git a/lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart index 045c674..ea8848c 100644 --- a/lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart @@ -4,8 +4,8 @@ import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_in class FirstPageErrorIndicator extends StatelessWidget { const FirstPageErrorIndicator({ this.onTryAgain, - Key? key, - }) : super(key: key); + super.key, + }); final VoidCallback? onTryAgain; diff --git a/lib/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart index f12538a..54fc686 100644 --- a/lib/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart @@ -6,8 +6,8 @@ class FirstPageExceptionIndicator extends StatelessWidget { required this.title, this.message, this.onTryAgain, - Key? key, - }) : super(key: key); + super.key, + }); final String title; final String? message; diff --git a/lib/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart index 3ce4476..f2645b2 100644 --- a/lib/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class FirstPageProgressIndicator extends StatelessWidget { - const FirstPageProgressIndicator({Key? key}) : super(key: key); + const FirstPageProgressIndicator({super.key}); @override Widget build(BuildContext context) => const Padding( diff --git a/lib/src/widgets/helpers/default_status_indicators/footer_tile.dart b/lib/src/widgets/helpers/default_status_indicators/footer_tile.dart index 4d49f07..83dbeb1 100644 --- a/lib/src/widgets/helpers/default_status_indicators/footer_tile.dart +++ b/lib/src/widgets/helpers/default_status_indicators/footer_tile.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; class FooterTile extends StatelessWidget { const FooterTile({ required this.child, - Key? key, - }) : super(key: key); + super.key, + }); final Widget child; diff --git a/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart index 8fcc597..2b1c4d9 100644 --- a/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart @@ -3,9 +3,9 @@ import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_in class NewPageErrorIndicator extends StatelessWidget { const NewPageErrorIndicator({ - Key? key, + super.key, this.onTap, - }) : super(key: key); + }); final VoidCallback? onTap; @override diff --git a/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart index bd24b07..55c13b4 100644 --- a/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart @@ -3,8 +3,8 @@ import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_in class NewPageProgressIndicator extends StatelessWidget { const NewPageProgressIndicator({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) => const FooterTile( diff --git a/lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart index 9c7b12d..e9786d6 100644 --- a/lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart'; class NoItemsFoundIndicator extends StatelessWidget { - const NoItemsFoundIndicator({Key? key}) : super(key: key); + const NoItemsFoundIndicator({super.key}); @override Widget build(BuildContext context) => const FirstPageExceptionIndicator( diff --git a/lib/src/widgets/helpers/paged_layout_builder.dart b/lib/src/widgets/helpers/paged_layout_builder.dart index 168d9f7..01d80ed 100644 --- a/lib/src/widgets/helpers/paged_layout_builder.dart +++ b/lib/src/widgets/helpers/paged_layout_builder.dart @@ -54,8 +54,8 @@ class PagedLayoutBuilder extends StatefulWidget { required this.completedListingBuilder, required this.layoutProtocol, this.shrinkWrapFirstPageIndicators = false, - Key? key, - }) : super(key: key); + super.key, + }); /// The controller for paged listings. /// @@ -293,8 +293,7 @@ class _FirstPageStatusIndicatorBuilder extends StatelessWidget { required this.builder, required this.layoutProtocol, this.shrinkWrap = false, - Key? key, - }) : super(key: key); + }); final WidgetBuilder builder; final bool shrinkWrap; diff --git a/lib/src/widgets/layouts/paged_aligned_grid_view.dart b/lib/src/widgets/layouts/paged_aligned_grid_view.dart index 51717af..5180f2e 100644 --- a/lib/src/widgets/layouts/paged_aligned_grid_view.dart +++ b/lib/src/widgets/layouts/paged_aligned_grid_view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -18,53 +17,40 @@ class PagedAlignedGridView extends BoxScrollView { required this.builderDelegate, required this.gridDelegateBuilder, // Matches [ScrollView.scrollDirection]. - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, this.scrollController, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, + super.clipBehavior, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Equivalent to [MasonryGridView.count]. @@ -72,57 +58,44 @@ class PagedAlignedGridView extends BoxScrollView { required this.pagingController, required this.builderDelegate, required int crossAxisCount, - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, this.scrollController, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, + super.clipBehavior, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, )), super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Equivalent to [MasonryGridView.extent]. @@ -130,57 +103,44 @@ class PagedAlignedGridView extends BoxScrollView { required this.pagingController, required this.builderDelegate, required double maxCrossAxisExtent, - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, this.scrollController, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, + super.clipBehavior, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, )), super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Matches [PagedLayoutBuilder.pagingController]. diff --git a/lib/src/widgets/layouts/paged_grid_view.dart b/lib/src/widgets/layouts/paged_grid_view.dart index bafb518..467cbc5 100644 --- a/lib/src/widgets/layouts/paged_grid_view.dart +++ b/lib/src/widgets/layouts/paged_grid_view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; @@ -17,50 +16,37 @@ class PagedGridView extends BoxScrollView { // Matches [ScrollView.controller]. ScrollController? scrollController, // Matches [ScrollView.scrollDirection]. - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, - Key? key, + super.clipBehavior, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Matches [PagedLayoutBuilder.pagingController]. diff --git a/lib/src/widgets/layouts/paged_list_view.dart b/lib/src/widgets/layouts/paged_list_view.dart index a6ec35a..579047f 100644 --- a/lib/src/widgets/layouts/paged_list_view.dart +++ b/lib/src/widgets/layouts/paged_list_view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; @@ -18,34 +17,33 @@ class PagedListView extends BoxScrollView { // Matches [ScrollView.controller]. ScrollController? scrollController, // Matches [ScrollView.scrollDirection]. - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.itemExtent, this.prototypeItem, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, // Matches [ScrollView.cacheExtent] - double? cacheExtent, + super.cacheExtent, // Matches [ScrollView.dragStartBehavior] - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior] - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId] - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior] - Clip clipBehavior = Clip.hardEdge, - Key? key, + super.clipBehavior, + super.key, }) : assert( itemExtent == null || prototypeItem == null, 'You can only pass itemExtent or prototypeItem, not both', @@ -53,19 +51,7 @@ class PagedListView extends BoxScrollView { _separatorBuilder = null, _shrinkWrapFirstPageIndicators = shrinkWrap, super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); const PagedListView.separated({ @@ -75,50 +61,37 @@ class PagedListView extends BoxScrollView { // Matches [ScrollView.controller]. ScrollController? scrollController, // Matches [ScrollView.scrollDirection]. - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.itemExtent, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, // Matches [ScrollView.cacheExtent] - double? cacheExtent, + super.cacheExtent, // Matches [ScrollView.dragStartBehavior] - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior] - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId] - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior] - Clip clipBehavior = Clip.hardEdge, - Key? key, + super.clipBehavior, + super.key, }) : prototypeItem = null, _shrinkWrapFirstPageIndicators = shrinkWrap, _separatorBuilder = separatorBuilder, super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Matches [PagedLayoutBuilder.pagingController]. diff --git a/lib/src/widgets/layouts/paged_masonry_grid_view.dart b/lib/src/widgets/layouts/paged_masonry_grid_view.dart index f6f41e5..1d21ad6 100644 --- a/lib/src/widgets/layouts/paged_masonry_grid_view.dart +++ b/lib/src/widgets/layouts/paged_masonry_grid_view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -18,53 +17,40 @@ class PagedMasonryGridView extends BoxScrollView { required this.builderDelegate, required this.gridDelegateBuilder, // Matches [ScrollView.scrollDirection]. - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, this.scrollController, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, + super.clipBehavior, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Equivalent to [MasonryGridView.count]. @@ -72,57 +58,44 @@ class PagedMasonryGridView extends BoxScrollView { required this.pagingController, required this.builderDelegate, required int crossAxisCount, - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, this.scrollController, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, + super.clipBehavior, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, )), super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Equivalent to [MasonryGridView.extent]. @@ -130,57 +103,44 @@ class PagedMasonryGridView extends BoxScrollView { required this.pagingController, required this.builderDelegate, required double maxCrossAxisExtent, - Axis scrollDirection = Axis.vertical, + super.scrollDirection, // Matches [ScrollView.reverse]. - bool reverse = false, + super.reverse, // Matches [ScrollView.primary]. - bool? primary, + super.primary, // Matches [ScrollView.physics]. - ScrollPhysics? physics, + super.physics, this.scrollController, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, // Matches [ScrollView.cacheExtent]. - double? cacheExtent, + super.cacheExtent, this.showNewPageProgressIndicatorAsGridChild = true, this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, // Matches [ScrollView.dragStartBehavior]. - DragStartBehavior dragStartBehavior = DragStartBehavior.start, + super.dragStartBehavior, // Matches [ScrollView.keyboardDismissBehavior]. - ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = - ScrollViewKeyboardDismissBehavior.manual, + super.keyboardDismissBehavior, // Matches [ScrollView.restorationId]. - String? restorationId, + super.restorationId, // Matches [ScrollView.clipBehavior]. - Clip clipBehavior = Clip.hardEdge, + super.clipBehavior, // Matches [ScrollView.shrinkWrap]. - bool shrinkWrap = false, + super.shrinkWrap, // Matches [BoxScrollView.padding]. - EdgeInsetsGeometry? padding, + super.padding, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.addSemanticIndexes = true, - Key? key, + super.key, }) : _shrinkWrapFirstPageIndicators = shrinkWrap, gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, )), super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, controller: scrollController, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - cacheExtent: cacheExtent, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - clipBehavior: clipBehavior, ); /// Matches [PagedLayoutBuilder.pagingController]. diff --git a/lib/src/widgets/layouts/paged_page_view.dart b/lib/src/widgets/layouts/paged_page_view.dart index f3f171b..377e1e6 100644 --- a/lib/src/widgets/layouts/paged_page_view.dart +++ b/lib/src/widgets/layouts/paged_page_view.dart @@ -29,8 +29,8 @@ class PagedPageView extends StatelessWidget { this.pageSnapping = true, this.padEnds = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, - }) : super(key: key); + super.key, + }); /// Matches [PagedLayoutBuilder.pagingController]. final PagingController pagingController; diff --git a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart b/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart index ac2d6d2..647378f 100644 --- a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart @@ -26,8 +26,8 @@ class PagedSliverAlignedGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, - }) : super(key: key); + super.key, + }); PagedSliverAlignedGrid.count({ required this.pagingController, @@ -42,15 +42,14 @@ class PagedSliverAlignedGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, + super.key, }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, - )), - super(key: key); + )); PagedSliverAlignedGrid.extent({ - Key? key, + super.key, required this.pagingController, required this.builderDelegate, required double maxCrossAxisExtent, @@ -66,8 +65,7 @@ class PagedSliverAlignedGrid extends StatelessWidget { }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, - )), - super(key: key); + )); /// Matches [PagedLayoutBuilder.pagingController]. final PagingController pagingController; diff --git a/lib/src/widgets/layouts/paged_sliver_grid.dart b/lib/src/widgets/layouts/paged_sliver_grid.dart index 70b11a6..5289f9b 100644 --- a/lib/src/widgets/layouts/paged_sliver_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_grid.dart @@ -24,8 +24,8 @@ class PagedSliverGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, - }) : super(key: key); + super.key, + }); /// Matches [PagedLayoutBuilder.pagingController]. final PagingController pagingController; diff --git a/lib/src/widgets/layouts/paged_sliver_list.dart b/lib/src/widgets/layouts/paged_sliver_list.dart index 4f17fe0..ef892b8 100644 --- a/lib/src/widgets/layouts/paged_sliver_list.dart +++ b/lib/src/widgets/layouts/paged_sliver_list.dart @@ -25,13 +25,12 @@ class PagedSliverList extends StatelessWidget { this.prototypeItem, this.semanticIndexCallback, this.shrinkWrapFirstPageIndicators = false, - Key? key, + super.key, }) : assert( itemExtent == null || prototypeItem == null, 'You can only pass itemExtent or prototypeItem, not both', ), - _separatorBuilder = null, - super(key: key); + _separatorBuilder = null; const PagedSliverList.separated({ required this.pagingController, @@ -43,10 +42,9 @@ class PagedSliverList extends StatelessWidget { this.itemExtent, this.semanticIndexCallback, this.shrinkWrapFirstPageIndicators = false, - Key? key, + super.key, }) : prototypeItem = null, - _separatorBuilder = separatorBuilder, - super(key: key); + _separatorBuilder = separatorBuilder; /// Matches [PagedLayoutBuilder.pagingController]. final PagingController pagingController; diff --git a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart b/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart index 4404761..53881a4 100644 --- a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart @@ -30,10 +30,8 @@ class PagedSliverMasonryGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, - }) : super( - key: key, - ); + super.key, + }); /// Equivalent to [SliverMasonryGrid.count]. PagedSliverMasonryGrid.count({ @@ -49,14 +47,11 @@ class PagedSliverMasonryGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, + super.key, }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, - )), - super( - key: key, - ); + )); /// Equivalent to [SliverMasonryGrid.extent]. PagedSliverMasonryGrid.extent({ @@ -72,14 +67,11 @@ class PagedSliverMasonryGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - Key? key, + super.key, }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, - )), - super( - key: key, - ); + )); /// Matches [PagedLayoutBuilder.pagingController]. final PagingController pagingController; diff --git a/pubspec.yaml b/pubspec.yaml index 59f8faf..72f4ef7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,8 +4,8 @@ version: 4.1.0 homepage: https://github.com/EdsonBueno/infinite_scroll_pagination environment: - sdk: ">=2.14.0 <4.0.0" - flutter: ">=1.22.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: From 90d040f07941abd8afe6918ccdd111b88b13a9b3 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 13 Oct 2024 16:35:29 +0200 Subject: [PATCH 02/37] refactor: move paging status to extension --- lib/src/model/paging_state.dart | 70 ++++---------------------------- lib/src/model/paging_status.dart | 53 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/lib/src/model/paging_state.dart b/lib/src/model/paging_state.dart index 092148e..757c845 100644 --- a/lib/src/model/paging_state.dart +++ b/lib/src/model/paging_state.dart @@ -1,8 +1,7 @@ import 'package:flutter/foundation.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_status.dart'; -/// The current item's list, error, and next page key state for a paginated -/// widget. +/// The current item's list, error, and next page key +/// for a paginated widget. @immutable class PagingState { const PagingState({ @@ -20,45 +19,17 @@ class PagingState { /// The key for the next page to be fetched. final PageKeyType? nextPageKey; - /// The current pagination status. - PagingStatus get status { - if (_isOngoing) { - return PagingStatus.ongoing; - } - - if (_isCompleted) { - return PagingStatus.completed; - } - - if (_isLoadingFirstPage) { - return PagingStatus.loadingFirstPage; - } - - if (_hasSubsequentPageError) { - return PagingStatus.subsequentPageError; - } - - if (_isEmpty) { - return PagingStatus.noItemsFound; - } else { - return PagingStatus.firstPageError; - } - } - @override - String toString() => - '${objectRuntimeType(this, 'PagingState')}(itemList: \u2524' - '$itemList\u251C, error: $error, nextPageKey: $nextPageKey)'; + String toString() => '${objectRuntimeType(this, 'PagingState')}' + '(itemList: \u2524$itemList\u251C, error: $error, nextPageKey: $nextPageKey)'; @override bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - return other is PagingState && - other.itemList == itemList && - other.error == error && - other.nextPageKey == nextPageKey; + return identical(this, other) || + (other is PagingState && + other.itemList == itemList && + other.error == error && + other.nextPageKey == nextPageKey); } @override @@ -67,27 +38,4 @@ class PagingState { error.hashCode, nextPageKey.hashCode, ); - - int? get _itemCount => itemList?.length; - - bool get _hasNextPage => nextPageKey != null; - - bool get _hasItems { - final itemCount = _itemCount; - return itemCount != null && itemCount > 0; - } - - bool get _hasError => error != null; - - bool get _isListingUnfinished => _hasItems && _hasNextPage; - - bool get _isOngoing => _isListingUnfinished && !_hasError; - - bool get _isCompleted => _hasItems && !_hasNextPage; - - bool get _isLoadingFirstPage => _itemCount == null && !_hasError; - - bool get _hasSubsequentPageError => _isListingUnfinished && _hasError; - - bool get _isEmpty => _itemCount != null && _itemCount == 0; } diff --git a/lib/src/model/paging_status.dart b/lib/src/model/paging_status.dart index ccc279e..162614a 100644 --- a/lib/src/model/paging_status.dart +++ b/lib/src/model/paging_status.dart @@ -1,3 +1,5 @@ +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + /// All possible status for a pagination. enum PagingStatus { completed, @@ -7,3 +9,54 @@ enum PagingStatus { firstPageError, subsequentPageError, } + +/// Extension methods for [PagingState] to determine the current status. +extension PagingStatusExtension on PagingState { + int? get _itemCount => itemList?.length; + + bool get _hasNextPage => nextPageKey != null; + + bool get _hasItems { + final itemCount = _itemCount; + return itemCount != null && itemCount > 0; + } + + bool get _hasError => error != null; + + bool get _isListingUnfinished => _hasItems && _hasNextPage; + + bool get _isOngoing => _isListingUnfinished && !_hasError; + + bool get _isCompleted => _hasItems && !_hasNextPage; + + bool get _isLoadingFirstPage => _itemCount == null && !_hasError; + + bool get _hasSubsequentPageError => _isListingUnfinished && _hasError; + + bool get _isEmpty => _itemCount != null && _itemCount == 0; + + /// The current pagination status. + PagingStatus get status { + if (_isOngoing) { + return PagingStatus.ongoing; + } + + if (_isCompleted) { + return PagingStatus.completed; + } + + if (_isLoadingFirstPage) { + return PagingStatus.loadingFirstPage; + } + + if (_hasSubsequentPageError) { + return PagingStatus.subsequentPageError; + } + + if (_isEmpty) { + return PagingStatus.noItemsFound; + } else { + return PagingStatus.firstPageError; + } + } +} From 4d2cc5776115ed07eb7210a9ca004e54dca691ee Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 13 Oct 2024 18:01:42 +0200 Subject: [PATCH 03/37] fix: remove pubspec.lock --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cbc2fdb..617b760 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,4 @@ build/ !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages # flutter library -/pubspec.lock \ No newline at end of file +/pubspec.lock From 206a8743e694995c369dae83cd8267ad2be37b93 Mon Sep 17 00:00:00 2001 From: clragon Date: Sat, 19 Oct 2024 04:07:46 +0200 Subject: [PATCH 04/37] refactor: rewrite page controller and paging state --- example/.metadata | 22 +- example/lib/common/error.dart | 63 ++-- example/lib/common/listing_bloc.dart | 80 +++--- example/lib/samples/list_view.dart | 78 ++--- example/lib/samples/page_view.dart | 148 +++++----- example/lib/samples/sliver_grid.dart | 203 +++++++------ lib/infinite_scroll_pagination.dart | 1 + .../core/paged_child_builder_delegate.dart | 6 +- lib/src/core/paging_controller.dart | 272 ++++++------------ lib/src/model/paging_state.dart | 100 +++++-- lib/src/model/paging_state_base.dart | 103 +++++++ lib/src/model/paging_status.dart | 8 +- lib/src/utils/appended_sliver_grid.dart | 26 +- .../new_page_error_indicator.dart | 1 + .../new_page_progress_indicator.dart | 4 +- .../widgets/helpers/paged_layout_builder.dart | 255 ++++++++-------- .../layouts/paged_aligned_grid_view.dart | 22 +- lib/src/widgets/layouts/paged_grid_view.dart | 18 +- lib/src/widgets/layouts/paged_list_view.dart | 24 +- .../layouts/paged_masonry_grid_view.dart | 22 +- lib/src/widgets/layouts/paged_page_view.dart | 16 +- .../layouts/paged_sliver_aligned_grid.dart | 26 +- .../widgets/layouts/paged_sliver_grid.dart | 18 +- .../widgets/layouts/paged_sliver_list.dart | 31 +- .../layouts/paged_sliver_masonry_grid.dart | 26 +- lib/src/widgets/state/paging_listener.dart | 33 +++ pubspec.yaml | 2 +- 27 files changed, 881 insertions(+), 727 deletions(-) create mode 100644 lib/src/model/paging_state_base.dart create mode 100644 lib/src/widgets/state/paging_listener.dart diff --git a/example/.metadata b/example/.metadata index cc3fb11..ea59ad0 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "300451adae589accbece3490f4396f10bdf15e6e" + revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" channel: "stable" project_type: app @@ -13,23 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 300451adae589accbece3490f4396f10bdf15e6e - base_revision: 300451adae589accbece3490f4396f10bdf15e6e - - platform: android - create_revision: 300451adae589accbece3490f4396f10bdf15e6e - base_revision: 300451adae589accbece3490f4396f10bdf15e6e - - platform: ios - create_revision: 300451adae589accbece3490f4396f10bdf15e6e - base_revision: 300451adae589accbece3490f4396f10bdf15e6e - - platform: linux - create_revision: 300451adae589accbece3490f4396f10bdf15e6e - base_revision: 300451adae589accbece3490f4396f10bdf15e6e - - platform: web - create_revision: 300451adae589accbece3490f4396f10bdf15e6e - base_revision: 300451adae589accbece3490f4396f10bdf15e6e + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: windows - create_revision: 300451adae589accbece3490f4396f10bdf15e6e - base_revision: 300451adae589accbece3490f4396f10bdf15e6e + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 # User provided section diff --git a/example/lib/common/error.dart b/example/lib/common/error.dart index 8db2110..e7aedb8 100644 --- a/example/lib/common/error.dart +++ b/example/lib/common/error.dart @@ -11,41 +11,44 @@ class CustomFirstPageError extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Something went wrong :(', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - if (pagingController.error != null) ...[ - const SizedBox( - height: 16, - ), + return PagingListener( + controller: pagingController, + builder: (context, state, _) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Text( - pagingController.error.toString(), + 'Something went wrong :(', textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, ), - ], - const SizedBox( - height: 48, - ), - SizedBox( - width: 200, - child: ElevatedButton.icon( - onPressed: pagingController.refresh, - icon: const Icon(Icons.refresh), - label: const Text( - 'Try Again', - style: TextStyle( - fontSize: 16, + if (state.error != null) ...[ + const SizedBox( + height: 16, + ), + Text( + state.error.toString(), + textAlign: TextAlign.center, + ), + ], + const SizedBox( + height: 48, + ), + SizedBox( + width: 200, + child: ElevatedButton.icon( + onPressed: pagingController.refresh, + icon: const Icon(Icons.refresh), + label: const Text( + 'Try Again', + style: TextStyle( + fontSize: 16, + ), ), ), ), - ), - ], + ], + ), ), ); } @@ -62,7 +65,7 @@ class CustomNewPageError extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: pagingController.retryLastFailedRequest, + onTap: pagingController.fetchNextPage, child: Padding( padding: const EdgeInsets.all(20), child: Column( diff --git a/example/lib/common/listing_bloc.dart b/example/lib/common/listing_bloc.dart index 693b4da..0660776 100644 --- a/example/lib/common/listing_bloc.dart +++ b/example/lib/common/listing_bloc.dart @@ -2,87 +2,81 @@ import 'dart:async'; import 'package:infinite_example/remote/item.dart'; import 'package:infinite_example/remote/api.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:rxdart/rxdart.dart'; -class ListingState { - ListingState({ - this.itemList, - this.error, - this.nextPageKey = 1, - }); - - final List? itemList; - final dynamic error; - final int? nextPageKey; -} - -class ListingBloc { - ListingBloc() { +class PhotoPagesBloc { + PhotoPagesBloc() { _onPageRequest.stream - .flatMap(_fetch) - .listen(_onNewListingStateController.add) + .flatMap((_) => _fetch((_stateController.value.keys?.last ?? 0) + 1)) + .listen(_stateController.add) .addTo(_subscriptions); - _onSearchInputChangedSubject.stream + _onSearchChanged.stream .flatMap((_) => _refresh()) - .listen(_onNewListingStateController.add) + .listen(_stateController.add) .addTo(_subscriptions); } final _subscriptions = CompositeSubscription(); - final _onNewListingStateController = BehaviorSubject.seeded( - ListingState(), + final _stateController = BehaviorSubject>.seeded( + PagingState(), ); - Stream get onNewListingState => - _onNewListingStateController.stream; + PagingState get state => _stateController.value; - final _onPageRequest = StreamController(); + Stream> get onState => _stateController.stream; - Sink get onPageRequestSink => _onPageRequest.sink; + final _onPageRequest = StreamController(); - final _onSearchInputChangedSubject = BehaviorSubject.seeded(null); + void fetchNextPage() => _onPageRequest.add(null); - Sink get onSearchInputChangedSink => - _onSearchInputChangedSubject.sink; + final _onSearchChanged = BehaviorSubject.seeded(null); - String? get _searchInputValue => _onSearchInputChangedSubject.value; + void changeSearch(String? value) => _onSearchChanged.add(value); - Stream _refresh() async* { - yield ListingState(); + String? get _searchInputValue => _onSearchChanged.value; + + Stream> _refresh() async* { + yield _stateController.value.reset(); yield* _fetch(1); } - Stream _fetch(int pageKey) async* { - final lastListingState = _onNewListingStateController.value; + Stream> _fetch(int pageKey) async* { + final lastListingState = _stateController.value; + yield lastListingState.copyWith( + error: null, + isLoading: true, + ); try { final newItems = await RemoteApi.getPhotos( pageKey, search: _searchInputValue, ); final isLastPage = newItems.isEmpty; - final nextPageKey = isLastPage ? null : pageKey + 1; - yield ListingState( + yield lastListingState.copyWith( error: null, - nextPageKey: nextPageKey, - itemList: [ - ...lastListingState.itemList ?? [], - ...newItems, + hasNextPage: !isLastPage, + pages: [ + ...lastListingState.pages ?? [], + newItems, + ], + keys: [ + ...lastListingState.keys ?? [], + pageKey, ], ); } catch (e) { - yield ListingState( + yield lastListingState.copyWith( error: e, - nextPageKey: lastListingState.nextPageKey, - itemList: lastListingState.itemList, ); } } void dispose() { - _onSearchInputChangedSubject.close(); - _onNewListingStateController.close(); + _onSearchChanged.close(); + _stateController.close(); _subscriptions.dispose(); _onPageRequest.close(); } diff --git a/example/lib/samples/list_view.dart b/example/lib/samples/list_view.dart index 609f702..37a631e 100644 --- a/example/lib/samples/list_view.dart +++ b/example/lib/samples/list_view.dart @@ -14,36 +14,28 @@ class ListViewScreen extends StatefulWidget { } class _ListViewScreenState extends State { - final PagingController _pagingController = - PagingController(firstPageKey: 1); - String? _searchTerm; + /// This example uses a [PagingController] to manage the paging state. + /// + /// This is a robust inbuilt way to store your pagination state. + /// The controller can also be used in multiple Paged layouts simultaneously, + /// to share their state. + late final _pagingController = PagingController( + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, + fetchPage: (pageKey) => RemoteApi.getPhotos(pageKey, search: _searchTerm), + ); + @override void initState() { super.initState(); - _pagingController.addPageRequestListener(_fetchPage); - _pagingController.addStatusListener(_showError); - } - - Future _fetchPage(int pageKey) async { - try { - final newItems = await RemoteApi.getPhotos(pageKey, search: _searchTerm); - - final isLastPage = newItems.isEmpty; - if (isLastPage) { - _pagingController.appendLastPage(newItems); - } else { - final nextPageKey = pageKey + 1; - _pagingController.appendPage(newItems, nextPageKey); - } - } catch (error) { - _pagingController.error = error; - } + _pagingController.addListener(_showError); } - Future _showError(PagingStatus status) async { - if (status == PagingStatus.subsequentPageError) { + /// This method listens to notifications from the [_pagingController] and + /// shows a [SnackBar] when an error occurs. + Future _showError() async { + if (_pagingController.value.status == PagingStatus.subsequentPageError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( @@ -51,18 +43,21 @@ class _ListViewScreenState extends State { ), action: SnackBarAction( label: 'Retry', - onPressed: () => _pagingController.retryLastFailedRequest(), + onPressed: () => _pagingController.fetchNextPage(), ), ), ); } } + /// When the search term changes, the controller is refreshed. + /// The refresh will remove all existing items and fetch the first page again. void _updateSearchTerm(String searchTerm) { setState(() => _searchTerm = searchTerm); _pagingController.refresh(); } + /// The controller needs to be disposed when the widget is removed. @override void dispose() { _pagingController.dispose(); @@ -77,19 +72,32 @@ class _ListViewScreenState extends State { ), body: RefreshIndicator( onRefresh: () async => _pagingController.refresh(), - child: PagedListView.separated( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - animateTransitions: true, - itemBuilder: (context, item, index) => ImageListTile( - item: item, + + /// The [PagingListener] is a widget that listens to the controller and + /// rebuilds the UI based on the state of the controller. + /// Its the easiest way to bind your controller to a Paged layout. + child: PagingListener( + controller: _pagingController, + builder: (context, state, fetchNextPage) => + + /// Paged layouts rely on a [PagingState] and a [fetchNextPage] function. + PagedListView.separated( + state: state, + fetchNextPage: fetchNextPage, + itemExtent: 48, + builderDelegate: PagedChildBuilderDelegate( + animateTransitions: true, + itemBuilder: (context, item, index) => ImageListTile( + key: ValueKey(item.id), + item: item, + ), + firstPageErrorIndicatorBuilder: (context) => + CustomFirstPageError(pagingController: _pagingController), + newPageErrorIndicatorBuilder: (context) => + CustomNewPageError(pagingController: _pagingController), ), - firstPageErrorIndicatorBuilder: (context) => - CustomFirstPageError(pagingController: _pagingController), - newPageErrorIndicatorBuilder: (context) => - CustomNewPageError(pagingController: _pagingController), + separatorBuilder: (context, index) => const Divider(), ), - separatorBuilder: (context, index) => const Divider(), ), ), ); diff --git a/example/lib/samples/page_view.dart b/example/lib/samples/page_view.dart index 4db1cec..81e4edc 100644 --- a/example/lib/samples/page_view.dart +++ b/example/lib/samples/page_view.dart @@ -12,85 +12,97 @@ class PageViewScreen extends StatefulWidget { } class _PageViewScreenState extends State { - final PagingController _pagingController = PagingController( - firstPageKey: 1, - ); final PageController _pageController = PageController(); - @override - void initState() { - super.initState(); - _pagingController.addPageRequestListener(_fetchPage); - } + PagingState _state = PagingState(); - Future _fetchPage(int pageKey) async { - try { - final newItems = await RemoteApi.getPhotos(pageKey); + void fetchNextPage() async { + if (_state.isLoading) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _state = _state.copyWith(isLoading: true, error: null); + }); + }); + + try { + final newKey = (_state.keys?.last ?? 0) + 1; + final newItems = await RemoteApi.getPhotos(newKey); final isLastPage = newItems.isEmpty; - if (isLastPage) { - _pagingController.appendLastPage(newItems); - } else { - final nextPageKey = pageKey + 1; - _pagingController.appendPage(newItems, nextPageKey); - } + + setState(() { + _state = _state.copyWith( + pages: [ + ...?_state.pages, + newItems, + ], + keys: [ + ...?_state.keys, + newKey, + ], + hasNextPage: !isLastPage, + isLoading: false, + ); + }); } catch (error) { - _pagingController.error = error; + setState(() { + _state = _state.copyWith( + error: error, + isLoading: false, + ); + }); } } @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => Stack( - fit: StackFit.passthrough, - children: [ - PagedPageView( - pageController: _pageController, - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => CachedNetworkImage( - imageUrl: item.thumbnail, - ), + Widget build(BuildContext context) { + return Stack( + fit: StackFit.passthrough, + children: [ + PagedPageView( + pageController: _pageController, + state: _state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => CachedNetworkImage( + imageUrl: item.thumbnail, ), ), - Positioned( - right: 0, - left: 0, - bottom: 16, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - borderRadius: BorderRadius.circular(4), - color: Colors.black38, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - child: ListenableBuilder( - listenable: _pageController, - builder: (context, _) { - if (_pageController.positions.isEmpty) { - return const SizedBox.shrink(); - } - return Text( - '${_pageController.page?.round()} / ${_pagingController.itemList?.length ?? 0}', - style: - Theme.of(context).textTheme.titleLarge?.copyWith( - color: Colors.white, - ), - ); - }, - ), - ), - ) - ], - ), + ), + Positioned( + right: 0, + left: 0, + bottom: 16, + child: ListenableBuilder( + listenable: _pageController, + builder: (context, _) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_pageController.hasClients) + Material( + borderRadius: BorderRadius.circular(4), + color: Colors.black38, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + child: Text( + '${(_pageController.page ?? 0).round()} / ${_state.items?.length ?? 0}', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: Colors.white), + ), + ), + ) + ], + ); + }, ), - ], - ); + ), + ], + ); + } } diff --git a/example/lib/samples/sliver_grid.dart b/example/lib/samples/sliver_grid.dart index 7a38aa0..4073893 100644 --- a/example/lib/samples/sliver_grid.dart +++ b/example/lib/samples/sliver_grid.dart @@ -1,4 +1,4 @@ -import 'dart:async'; +import 'dart:math'; import 'package:infinite_example/remote/item.dart'; import 'package:infinite_example/common/listing_bloc.dart'; @@ -21,117 +21,132 @@ class SliverGridScreen extends StatefulWidget { } class _SliverGridScreenState extends State { - final ListingBloc _bloc = ListingBloc(); - final PagingController _pagingController = - PagingController(firstPageKey: 1); - late StreamSubscription _blocListingStateSubscription; - _GridType _gridType = _GridType.square; - - @override - void initState() { - super.initState(); - _pagingController.addPageRequestListener((pageKey) { - _bloc.onPageRequestSink.add(pageKey); - }); + /// This example uses a [PhotoPagesBloc] to manage the paging state. + /// + /// The [PhotoPagesBloc] is a custom class we wrote to manage the paging state. + /// Paged layouts are not limited to using a [PagingController]. + /// You can use any state management solution you prefer. + /// + /// In this case, [PhotoPagesBloc] is a bloc class built with RxDart. + final PhotoPagesBloc _bloc = PhotoPagesBloc(); - _blocListingStateSubscription = - _bloc.onNewListingState.listen((listingState) { - _pagingController.value = PagingState( - nextPageKey: listingState.nextPageKey, - error: listingState.error, - itemList: listingState.itemList, - ); - }); - } + _GridType _gridType = _GridType.square; @override void dispose() { - _pagingController.dispose(); - _blocListingStateSubscription.cancel(); _bloc.dispose(); super.dispose(); } @override - Widget build(BuildContext context) => SafeArea( - child: CustomScrollView( - slivers: [ - SearchInputSliver( - onChanged: (searchTerm) => _bloc.onSearchInputChangedSink.add( - searchTerm, - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(8), - child: SingleChildScrollView( - child: Row( - children: [ - for (final gridType in _GridType.values) ...[ - ChoiceChip( - selected: _gridType == gridType, - onSelected: (value) => - setState(() => _gridType = gridType), - label: Text( - gridType.name.split('').first.toUpperCase() + - gridType.name.substring(1)), - ), - const SizedBox(width: 8), - ], - ], + Widget build(BuildContext context) => StreamBuilder>( + stream: _bloc.onState, + initialData: _bloc.state, + builder: (context, snapshot) => LayoutBuilder( + builder: (context, constraints) => SafeArea( + child: CustomScrollView( + slivers: [ + SearchInputSliver( + onChanged: (searchTerm) => _bloc.changeSearch( + searchTerm, ), ), - ), - ), - switch (_gridType) { - _GridType.square => PagedSliverGrid( - pagingController: _pagingController, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - childAspectRatio: 1 / 1.2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - maxCrossAxisExtent: 200, - ), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => CachedNetworkImage( - imageUrl: item.thumbnail, - fit: BoxFit.cover, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(8), + child: SingleChildScrollView( + child: Row( + children: [ + for (final gridType in _GridType.values) ...[ + ChoiceChip( + selected: _gridType == gridType, + onSelected: (value) => + setState(() => _gridType = gridType), + label: Text( + gridType.name.split('').first.toUpperCase() + + gridType.name.substring(1), + ), + ), + const SizedBox(width: 8), + ], + ], + ), ), ), ), - _GridType.masonry => PagedSliverMasonryGrid.extent( - pagingController: _pagingController, - maxCrossAxisExtent: 200, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => AspectRatio( - aspectRatio: item.width / item.height, - child: CachedNetworkImage( - imageUrl: item.thumbnail, + switch (_gridType) { + _GridType.square => PagedSliverGrid( + state: snapshot.data!, + fetchNextPage: _bloc.fetchNextPage, + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + childAspectRatio: 1 / 1.2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + maxCrossAxisExtent: 200, + ), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => + CachedNetworkImage( + imageUrl: item.thumbnail, + fit: BoxFit.cover, + ), ), ), - ), - ), - _GridType.aligned => PagedSliverAlignedGrid.extent( - pagingController: _pagingController, - maxCrossAxisExtent: 200, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => AspectRatio( - aspectRatio: item.width / item.height, - child: CachedNetworkImage( - imageUrl: item.thumbnail, + _GridType.masonry => + PagedSliverMasonryGrid.extent( + state: snapshot.data!, + fetchNextPage: _bloc.fetchNextPage, + maxCrossAxisExtent: 200, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => AspectRatio( + aspectRatio: item.width / item.height, + child: CachedNetworkImage( + imageUrl: item.thumbnail, + ), + ), ), ), - ), - showNewPageErrorIndicatorAsGridChild: false, - showNewPageProgressIndicatorAsGridChild: false, - showNoMoreItemsIndicatorAsGridChild: false, - ), - }, - ], + _GridType.aligned => + PagedSliverAlignedGrid.extent( + state: snapshot.data!, + fetchNextPage: _bloc.fetchNextPage, + maxCrossAxisExtent: 200, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + final rowCount = constraints.maxWidth ~/ 200; + final rowItems = snapshot.data!.items!.sublist( + max(0, index - index % rowCount), + min(snapshot.data!.items!.length, + index - index % rowCount + rowCount), + ); + final averageRatio = rowItems + .map((e) => e.width / e.height) + .reduce((a, b) => a + b) / + rowItems.length; + + return SizedBox( + // find out which row this item is in, then alculate the average height of the row + height: min(200 / averageRatio, 500), + child: CachedNetworkImage( + imageUrl: item.thumbnail, + fit: BoxFit.cover, + ), + ); + }, + ), + showNewPageErrorIndicatorAsGridChild: false, + showNewPageProgressIndicatorAsGridChild: false, + showNoMoreItemsIndicatorAsGridChild: false, + ), + }, + ], + ), + ), ), ); } diff --git a/lib/infinite_scroll_pagination.dart b/lib/infinite_scroll_pagination.dart index c896f2b..7fb2734 100644 --- a/lib/infinite_scroll_pagination.dart +++ b/lib/infinite_scroll_pagination.dart @@ -12,3 +12,4 @@ export 'src/widgets/layouts/paged_sliver_grid.dart'; export 'src/widgets/layouts/paged_sliver_list.dart'; export 'src/widgets/layouts/paged_sliver_masonry_grid.dart'; export 'src/widgets/layouts/paged_sliver_aligned_grid.dart'; +export 'src/widgets/state/paging_listener.dart'; diff --git a/lib/src/core/paged_child_builder_delegate.dart b/lib/src/core/paged_child_builder_delegate.dart index 1ca3002..f1fa732 100644 --- a/lib/src/core/paged_child_builder_delegate.dart +++ b/lib/src/core/paged_child_builder_delegate.dart @@ -10,7 +10,7 @@ typedef ItemWidgetBuilder = Widget Function( /// /// The generic type [ItemType] must be specified in order to properly identify /// the list item's type. -class PagedChildBuilderDelegate { +class PagedChildBuilderDelegate { const PagedChildBuilderDelegate({ required this.itemBuilder, this.firstPageErrorIndicatorBuilder, @@ -21,6 +21,7 @@ class PagedChildBuilderDelegate { this.noMoreItemsIndicatorBuilder, this.animateTransitions = false, this.transitionDuration = const Duration(milliseconds: 250), + this.invisibleItemsThreshold = 3, }); /// The builder for list items. @@ -49,4 +50,7 @@ class PagedChildBuilderDelegate { /// The duration of animated transitions when [animateTransitions] is `true`. final Duration transitionDuration; + + /// The number of remaining invisible items that should trigger a new fetch. + final int invisibleItemsThreshold; } diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index 228f9ad..95b0f63 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -1,226 +1,120 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_status.dart'; -typedef PageRequestListener = void Function( - PageKeyType pageKey, -); +/// A callback to get the next page key. +/// If this function returns `null`, it indicates that there are no more pages to load. +typedef NextPageKeyCallback + = PageKeyType? Function(PagingState state); -typedef PagingStatusListener = void Function( - PagingStatus status, -); +/// A callback to fetch a page. +typedef FetchPageCallback + = FutureOr> Function(PageKeyType pageKey); -/// A controller for a paged widget. -/// -/// If you modify the [itemList], [error] or [nextPageKey] properties, the -/// paged widget will be notified and will update itself appropriately. +/// A controller to handle a [PagingState]. /// -/// The [itemList], [error] or [nextPageKey] properties can be set from within -/// a listener added to this controller. If more than one property need to be -/// changed then the controller's [value] should be set instead. -/// -/// This object should generally have a lifetime longer than the widgets -/// itself; it should be reused each time a paged widget constructor is called. -class PagingController +/// This is an unopinionated controller implemented through vanilla Flutter's [ValueNotifier]. +/// The controller acts as a mutex to prevent multiple fetches at the same time. +class PagingController extends ValueNotifier> { PagingController({ - required this.firstPageKey, - this.invisibleItemsThreshold, - }) : super( - PagingState(nextPageKey: firstPageKey), + PagingState? value, + required NextPageKeyCallback getNextPageKey, + required FetchPageCallback fetchPage, + }) : _getNextPageKey = getNextPageKey, + _fetchPage = fetchPage, + super( + value ?? PagingState(), ); - /// Creates a controller from an existing [PagingState]. - /// - /// [firstPageKey] is the key to be used in case of a [refresh]. - PagingController.fromValue( - super.value, { - required this.firstPageKey, - this.invisibleItemsThreshold, - }); - - ObserverList? _statusListeners = - ObserverList(); - - ObserverList>? _pageRequestListeners = - ObserverList>(); - - /// The number of remaining invisible items that should trigger a new fetch. - final int? invisibleItemsThreshold; + /// The function to get the next page key. + /// If this function returns `null`, it indicates that there are no more pages to load. + final NextPageKeyCallback _getNextPageKey; - /// The key for the first page to be fetched. - final PageKeyType firstPageKey; + /// The function to fetch a page. + final FetchPageCallback _fetchPage; - /// List with all items loaded so far. Initially `null`. - List? get itemList => value.itemList; - - set itemList(List? newItemList) { - value = PagingState( - error: error, - itemList: newItemList, - nextPageKey: nextPageKey, - ); - } - - /// The current error, if any. Initially `null`. - dynamic get error => value.error; - - set error(dynamic newError) { - value = PagingState( - error: newError, - itemList: itemList, - nextPageKey: nextPageKey, - ); - } - - /// The key for the next page to be fetched. + /// Keeps track of the current operation. + /// If the operation changes during its execution, the operation is cancelled. /// - /// Initialized with the same value as [firstPageKey], received in the - /// constructor. - PageKeyType? get nextPageKey => value.nextPageKey; - - set nextPageKey(PageKeyType? newNextPageKey) { - value = PagingState( - error: error, - itemList: itemList, - nextPageKey: newNextPageKey, - ); - } + /// In more concrete terms, this cancels old fetch calls. + Object? _operation; - /// Corresponding to [ValueNotifier.value]. - @override - set value(PagingState newValue) { - if (value.status != newValue.status) { - notifyStatusListeners(newValue.status); - } + /// Fetches the next page. + /// + /// If called while a page is fetching or no more pages are available, this method does nothing. + void fetchNextPage() async { + // We are already loading a new page. + if (_operation != null) return; - super.value = newValue; - } + final operation = _operation = Object(); - /// Appends [newItems] to the previously loaded ones and replaces - /// the next page's key. - void appendPage(List newItems, PageKeyType? nextPageKey) { - final previousItems = value.itemList ?? []; - final itemList = previousItems + newItems; - value = PagingState( - itemList: itemList, + // we use a local copy of value, + // so that we only send one notification now and at the end of the method. + PagingState state = value = value.copyWith( + isLoading: true, error: null, - nextPageKey: nextPageKey, ); - } - /// Appends [newItems] to the previously loaded ones and sets the next page - /// key to `null`. - void appendLastPage(List newItems) => appendPage(newItems, null); + try { + // There are no more pages to load. + if (!state.hasNextPage) return; - /// Erases the current error. - void retryLastFailedRequest() { - error = null; - } + final nextPageKey = _getNextPageKey(state); - /// Resets [value] to its initial state. - void refresh() { - value = PagingState( - nextPageKey: firstPageKey, - error: null, - itemList: null, - ); - } - - bool _debugAssertNotDisposed() { - assert(() { - if (_pageRequestListeners == null || _statusListeners == null) { - throw Exception( - 'A PagingController was used after being disposed.\nOnce you have ' - 'called dispose() on a PagingController, it can no longer be ' - 'used.\nIf you’re using a Future, it probably completed after ' - 'the disposal of the owning widget.\nMake sure dispose() has not ' - 'been called yet before using the PagingController.', - ); + // We are at the end of the list. + if (nextPageKey == null) { + state = state.copyWith(hasNextPage: false); + return; } - return true; - }()); - return true; - } - /// Calls listener every time the status of the pagination changes. - /// - /// Listeners can be removed with [removeStatusListener]. - void addStatusListener(PagingStatusListener listener) { - assert(_debugAssertNotDisposed()); - _statusListeners?.add(listener); - } + final fetchResult = _fetchPage(nextPageKey); + List newItems; - /// Stops calling the listener every time the status of the pagination - /// changes. - /// - /// Listeners can be added with [addStatusListener]. - void removeStatusListener(PagingStatusListener listener) { - assert(_debugAssertNotDisposed()); - _statusListeners?.remove(listener); - } + // If the result is synchronous, we can directly assign it in the same tick. + if (fetchResult is Future) { + newItems = await fetchResult; + } else { + newItems = fetchResult; + } - /// Calls all the status listeners. - /// - /// If listeners are added or removed during this function, the modifications - /// will not change which listeners are called during this iteration. - void notifyStatusListeners(PagingStatus status) { - assert(_debugAssertNotDisposed()); + if (operation != _operation) return; - if (_statusListeners?.isEmpty ?? true) { - return; - } + state = state.copyWith( + pages: [...?state.pages, newItems], + keys: [...?state.keys, nextPageKey], + ); + } catch (error) { + state = state.copyWith(error: error); - final localListeners = List.from(_statusListeners!); - for (final listener in localListeners) { - if (_statusListeners!.contains(listener)) { - listener(status); + if (error is! Exception) { + // Errors which are not exceptions indicate that something + // went unexpectedly wrong. These errors are rethrown + // so they can be logged and investigated. + rethrow; + } + } finally { + value = state.copyWith(isLoading: false); + if (operation == _operation) { + _operation = null; } } } - /// Calls listener every time new items are needed. + /// Restarts the pagination process. /// - /// Listeners can be removed with [removePageRequestListener]. - void addPageRequestListener(PageRequestListener listener) { - assert(_debugAssertNotDisposed()); - _pageRequestListeners?.add(listener); - } - - /// Stops calling the listener every time new items are needed. - /// - /// Listeners can be added with [addPageRequestListener]. - void removePageRequestListener(PageRequestListener listener) { - assert(_debugAssertNotDisposed()); - _pageRequestListeners?.remove(listener); + /// This cancels the current fetch operation and resets the state. + void refresh() { + _operation = null; + value = value.reset(); } - /// Calls all the page request listeners. + /// Cancels the current fetch operation. /// - /// If listeners are added or removed during this function, the modifications - /// will not change which listeners are called during this iteration. - void notifyPageRequestListeners(PageKeyType pageKey) { - assert(_debugAssertNotDisposed()); - - if (_pageRequestListeners?.isEmpty ?? true) { - return; - } - - final localListeners = - List>.from(_pageRequestListeners!); - - for (final listener in localListeners) { - if (_pageRequestListeners!.contains(listener)) { - listener(pageKey); - } - } - } - - @override - void dispose() { - assert(_debugAssertNotDisposed()); - _statusListeners = null; - _pageRequestListeners = null; - super.dispose(); + /// This can be called right before a call to [fetchNextPage] to force a new fetch. + void cancel() { + _operation = null; + value = value.copyWith(isLoading: false); } } diff --git a/lib/src/model/paging_state.dart b/lib/src/model/paging_state.dart index 757c845..504366b 100644 --- a/lib/src/model/paging_state.dart +++ b/lib/src/model/paging_state.dart @@ -1,41 +1,81 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state_base.dart'; -/// The current item's list, error, and next page key -/// for a paginated widget. +/// Represents the state of a paginated layout. @immutable -class PagingState { - const PagingState({ - this.nextPageKey, - this.itemList, - this.error, - }); +abstract class PagingState { + /// Creates a [PagingState] with the given parameters. + factory PagingState({ + List>? pages, + List? keys, + Object? error, + bool hasNextPage, + }) = PagingStateBase; - /// List with all items loaded so far. - final List? itemList; + /// The pages fetched so far. + /// + /// This contains all pages fetched so far. + /// The corresponding key for each page is at the same index in [keys]. + List>? get pages; - /// The current error, if any. - final dynamic error; + /// The keys of the pages fetched so far. + /// + /// This contains all keys used to fetch pages so far. + /// The corresponding page for each key is at the same index in [pages]. + List? get keys; - /// The key for the next page to be fetched. - final PageKeyType? nextPageKey; + /// The last error that occurred while fetching a page. + /// This is null if no error occurred. + Object? get error; - @override - String toString() => '${objectRuntimeType(this, 'PagingState')}' - '(itemList: \u2524$itemList\u251C, error: $error, nextPageKey: $nextPageKey)'; + /// Will be `true` if there is a next page to be fetched. + bool get hasNextPage; - @override - bool operator ==(Object other) { - return identical(this, other) || - (other is PagingState && - other.itemList == itemList && - other.error == error && - other.nextPageKey == nextPageKey); - } + /// Will be `true` if a page is currently being fetched. + bool get isLoading; + + /// Creates a copy of this [PagingState] but with the given fields replaced by the new values. + /// If a field is not provided, it will default to the current value. + /// + /// While this implementation technically accepts Futures, passing a Future is invalid. + /// The FutureOr type is used to allow for the Omit sentinel value, + /// which is required to distinguish between a parameter being omitted and a parameter being set to null. + // copyWith a la Remi Rousselet: https://github.com/dart-lang/language/issues/137#issuecomment-583783054 + PagingState copyWith({ + FutureOr>?>? pages = const Omit(), + FutureOr?>? keys = const Omit(), + FutureOr? error = const Omit(), + FutureOr? hasNextPage = const Omit(), + FutureOr? isLoading = const Omit(), + }); + + /// Returns a copy this [PagingState] but + /// all fields are reset to their initial values. + /// + /// If you are implementing a custom [PagingState], you should override this method + /// to reset custom fields as well. + /// + /// The reason we use this instead of creating a new instance is so that + /// a custom [PagingState] can be reset without losing its type. + PagingState reset(); +} + +extension ItemListExtension + on PagingState { + /// The list of all items in the pages. + List? get items => pages?.expand((e) => e).toList(); +} + +/// Sentinel value to omit a parameter from a copyWith call. +/// This is used to distinguish between a parameter being omitted and a parameter +/// being set to null. +/// See https://github.com/dart-lang/language/issues/140 for why this is necessary. +final class Omit implements Future { + const Omit(); @override - int get hashCode => Object.hash( - itemList.hashCode, - error.hashCode, - nextPageKey.hashCode, - ); + noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } diff --git a/lib/src/model/paging_state_base.dart b/lib/src/model/paging_state_base.dart new file mode 100644 index 0000000..2687873 --- /dev/null +++ b/lib/src/model/paging_state_base.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; + +/// The default implementation of [PagingState]. +/// +/// This class is equal to another instance of [PagingStateBase] if +/// all of its fields are deeply equal. +base class PagingStateBase + implements PagingState { + factory PagingStateBase({ + List>? pages, + List? keys, + Object? error, + bool hasNextPage = true, + bool isLoading = false, + }) => + PagingStateBase._( + pages: switch (pages) { + null => null, + _ => List.unmodifiable(pages), + }, + keys: switch (keys) { + null => null, + _ => List.unmodifiable(keys), + }, + error: error, + hasNextPage: hasNextPage, + isLoading: isLoading, + ); + + const PagingStateBase._({ + required this.pages, + required this.keys, + required this.error, + required this.hasNextPage, + required this.isLoading, + }); + + @override + final List>? pages; + + @override + final List? keys; + + @override + final Object? error; + + @override + final bool hasNextPage; + + @override + final bool isLoading; + + @override + PagingState copyWith({ + FutureOr>?>? pages = const Omit(), + FutureOr?>? keys = const Omit(), + FutureOr? error = const Omit(), + FutureOr? hasNextPage = const Omit(), + FutureOr? isLoading = const Omit(), + }) => + PagingStateBase( + pages: pages is Omit ? this.pages : pages as List>?, + keys: keys is Omit ? this.keys : keys as List?, + error: error is Omit ? this.error : error as Object?, + hasNextPage: + hasNextPage is Omit ? this.hasNextPage : hasNextPage as bool, + isLoading: isLoading is Omit ? this.isLoading : isLoading as bool, + ); + + @override + PagingState reset() => PagingStateBase( + pages: null, + keys: null, + error: null, + hasNextPage: true, + isLoading: false, + ); + + @override + String toString() => '${objectRuntimeType(this, 'PagingState')}' + '(pages: $pages, keys: $keys, error: $error, hasNextPage: $hasNextPage)'; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is PagingState && + listEquals(other.pages, pages) && + listEquals(other.keys, keys) && + other.error == error && + other.hasNextPage == hasNextPage); + } + + @override + int get hashCode => Object.hash( + pages, + keys, + error, + hasNextPage, + ); +} diff --git a/lib/src/model/paging_status.dart b/lib/src/model/paging_status.dart index 162614a..abe17bd 100644 --- a/lib/src/model/paging_status.dart +++ b/lib/src/model/paging_status.dart @@ -12,9 +12,7 @@ enum PagingStatus { /// Extension methods for [PagingState] to determine the current status. extension PagingStatusExtension on PagingState { - int? get _itemCount => itemList?.length; - - bool get _hasNextPage => nextPageKey != null; + int? get _itemCount => items?.length; bool get _hasItems { final itemCount = _itemCount; @@ -23,11 +21,11 @@ extension PagingStatusExtension on PagingState { bool get _hasError => error != null; - bool get _isListingUnfinished => _hasItems && _hasNextPage; + bool get _isListingUnfinished => _hasItems && hasNextPage; bool get _isOngoing => _isListingUnfinished && !_hasError; - bool get _isCompleted => _hasItems && !_hasNextPage; + bool get _isCompleted => _hasItems && !hasNextPage; bool get _isLoadingFirstPage => _itemCount == null && !_hasError; diff --git a/lib/src/utils/appended_sliver_grid.dart b/lib/src/utils/appended_sliver_grid.dart index 3eb65fc..8c28a40 100644 --- a/lib/src/utils/appended_sliver_grid.dart +++ b/lib/src/utils/appended_sliver_grid.dart @@ -34,12 +34,24 @@ class AppendedSliverGrid extends StatelessWidget { Widget build(BuildContext context) { final appendixBuilder = this.appendixBuilder; + SliverChildBuilderDelegate buildSliverDelegate({ + WidgetBuilder? appendixBuilder, + }) => + AppendedSliverChildBuilderDelegate( + builder: itemBuilder, + childCount: itemCount, + appendixBuilder: appendixBuilder, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ); + return SliverMainAxisGroup( slivers: [ sliverGridBuilder( itemCount + (showAppendixAsGridChild && appendixBuilder != null ? 1 : 0), - _buildSliverDelegate( + buildSliverDelegate( appendixBuilder: showAppendixAsGridChild ? appendixBuilder : null, ), ), @@ -50,16 +62,4 @@ class AppendedSliverGrid extends StatelessWidget { ], ); } - - SliverChildBuilderDelegate _buildSliverDelegate({ - WidgetBuilder? appendixBuilder, - }) => - AppendedSliverChildBuilderDelegate( - builder: itemBuilder, - childCount: itemCount, - appendixBuilder: appendixBuilder, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: addRepaintBoundaries, - addSemanticIndexes: addSemanticIndexes, - ); } diff --git a/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart index 2b1c4d9..4bb77c8 100644 --- a/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart @@ -6,6 +6,7 @@ class NewPageErrorIndicator extends StatelessWidget { super.key, this.onTap, }); + final VoidCallback? onTap; @override diff --git a/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart b/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart index 55c13b4..920ea5a 100644 --- a/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart +++ b/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/footer_tile.dart'; class NewPageProgressIndicator extends StatelessWidget { - const NewPageProgressIndicator({ - super.key, - }); + const NewPageProgressIndicator({super.key}); @override Widget build(BuildContext context) => const FooterTile( diff --git a/lib/src/widgets/helpers/paged_layout_builder.dart b/lib/src/widgets/helpers/paged_layout_builder.dart index 01d80ed..8bc0df6 100644 --- a/lib/src/widgets/helpers/paged_layout_builder.dart +++ b/lib/src/widgets/helpers/paged_layout_builder.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:infinite_scroll_pagination/src/utils/listenable_listener.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart'; @@ -11,6 +10,9 @@ import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_in import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart'; import 'package:sliver_tools/sliver_tools.dart'; +/// Called to request a new page of data. +typedef NextPageCallback = VoidCallback; + typedef CompletedListingBuilder = Widget Function( BuildContext context, IndexedWidgetBuilder itemWidgetBuilder, @@ -45,9 +47,11 @@ enum PagedLayoutProtocol { sliver, box } /// For ordinary cases, this widget shouldn't be used directly. Instead, take a /// look at [PagedSliverList], [PagedSliverGrid], [PagedListView], /// [PagedGridView], [PagedMasonryGridView], or [PagedPageView]. -class PagedLayoutBuilder extends StatefulWidget { +class PagedLayoutBuilder + extends StatefulWidget { const PagedLayoutBuilder({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.loadingListingBuilder, required this.errorListingBuilder, @@ -57,11 +61,11 @@ class PagedLayoutBuilder extends StatefulWidget { super.key, }); - /// The controller for paged listings. - /// - /// Informs the current state of the pagination and requests new items from - /// its listeners. - final PagingController pagingController; + /// The paging state for this layout. + final PagingState state; + + /// A callback function that is triggered to request a new page of data. + final NextPageCallback fetchNextPage; /// The delegate for building the UI pieces of scrolling paged listings. final PagedChildBuilderDelegate builderDelegate; @@ -98,10 +102,12 @@ class PagedLayoutBuilder extends StatefulWidget { _PagedLayoutBuilderState(); } -class _PagedLayoutBuilderState +class _PagedLayoutBuilderState extends State> { - PagingController get _pagingController => - widget.pagingController; + PagingState get _state => widget.state; + + NextPageCallback get _fetchNextPage => widget.fetchNextPage; PagedChildBuilderDelegate get _builderDelegate => widget.builderDelegate; @@ -114,13 +120,13 @@ class _PagedLayoutBuilderState WidgetBuilder get _firstPageErrorIndicatorBuilder => _builderDelegate.firstPageErrorIndicatorBuilder ?? (_) => FirstPageErrorIndicator( - onTryAgain: _pagingController.retryLastFailedRequest, + onTryAgain: _fetchNextPage, ); WidgetBuilder get _newPageErrorIndicatorBuilder => _builderDelegate.newPageErrorIndicatorBuilder ?? (_) => NewPageErrorIndicator( - onTap: _pagingController.retryLastFailedRequest, + onTap: _fetchNextPage, ); WidgetBuilder get _firstPageProgressIndicatorBuilder => @@ -138,120 +144,119 @@ class _PagedLayoutBuilderState WidgetBuilder? get _noMoreItemsIndicatorBuilder => _builderDelegate.noMoreItemsIndicatorBuilder; - int get _invisibleItemsThreshold => - _pagingController.invisibleItemsThreshold ?? 3; - - int get _itemCount => _pagingController.itemCount; + int get _invisibleItemsThreshold => _builderDelegate.invisibleItemsThreshold; - bool get _hasNextPage => _pagingController.hasNextPage; + int get _itemCount => _state.items?.length ?? 0; - PageKeyType? get _nextKey => _pagingController.nextPageKey; + bool get _hasNextPage => _state.hasNextPage; /// Avoids duplicate requests on rebuilds. bool _hasRequestedNextPage = false; @override - Widget build(BuildContext context) => ListenableListener( - listenable: _pagingController, - listener: () { - final status = _pagingController.value.status; - - if (status == PagingStatus.loadingFirstPage) { - _pagingController.notifyPageRequestListeners( - _pagingController.firstPageKey, - ); - } - - if (status == PagingStatus.ongoing) { - _hasRequestedNextPage = false; - } - }, - child: ValueListenableBuilder>( - valueListenable: _pagingController, - builder: (context, pagingState, _) { - Widget child; - final itemList = _pagingController.itemList; - switch (pagingState.status) { - case PagingStatus.ongoing: - child = widget.loadingListingBuilder( - context, - // We must create this closure to close over the [itemList] - // value. That way, we are safe if [itemList] value changes - // while Flutter rebuilds the widget (due to animations, for - // example.) - (context, index) => _buildListItemWidget( - context, - index, - itemList!, - ), - _itemCount, - _newPageProgressIndicatorBuilder, - ); - break; - case PagingStatus.completed: - child = widget.completedListingBuilder( - context, - (context, index) => _buildListItemWidget( - context, - index, - itemList!, - ), - _itemCount, - _noMoreItemsIndicatorBuilder, - ); - break; - case PagingStatus.loadingFirstPage: - child = _FirstPageStatusIndicatorBuilder( - builder: _firstPageProgressIndicatorBuilder, - shrinkWrap: _shrinkWrapFirstPageIndicators, - layoutProtocol: _layoutProtocol, - ); - break; - case PagingStatus.subsequentPageError: - child = widget.errorListingBuilder( - context, - (context, index) => _buildListItemWidget( - context, - index, - itemList!, - ), - _itemCount, - (context) => _newPageErrorIndicatorBuilder(context), - ); - break; - case PagingStatus.noItemsFound: - child = _FirstPageStatusIndicatorBuilder( - builder: _noItemsFoundIndicatorBuilder, - shrinkWrap: _shrinkWrapFirstPageIndicators, - layoutProtocol: _layoutProtocol, - ); - break; - default: - child = _FirstPageStatusIndicatorBuilder( - builder: _firstPageErrorIndicatorBuilder, - shrinkWrap: _shrinkWrapFirstPageIndicators, - layoutProtocol: _layoutProtocol, - ); - } - - if (_builderDelegate.animateTransitions) { - if (_layoutProtocol == PagedLayoutProtocol.sliver) { - return SliverAnimatedSwitcher( - duration: _builderDelegate.transitionDuration, - child: child, - ); - } else { - return AnimatedSwitcher( - duration: _builderDelegate.transitionDuration, - child: child, - ); - } - } else { - return child; - } - }, - ), - ); + void initState() { + super.initState(); + if (_state.status == PagingStatus.loadingFirstPage) { + _fetchNextPage(); + } + } + + @override + void didUpdateWidget( + covariant PagedLayoutBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.state != widget.state) { + if (_state.status == PagingStatus.loadingFirstPage) { + _fetchNextPage(); + } else if (_state.status == PagingStatus.ongoing) { + _hasRequestedNextPage = false; + } + } + } + + @override + Widget build(BuildContext context) { + Widget child; + final items = _state.items; + switch (_state.status) { + case PagingStatus.ongoing: + child = widget.loadingListingBuilder( + context, + // We must create this closure to close over the [itemList] + // value. That way, we are safe if [itemList] value changes + // while Flutter rebuilds the widget (due to animations, for + // example.) + (context, index) => _buildListItemWidget( + context, + index, + items!, + ), + _itemCount, + _newPageProgressIndicatorBuilder, + ); + break; + case PagingStatus.completed: + child = widget.completedListingBuilder( + context, + (context, index) => _buildListItemWidget( + context, + index, + items!, + ), + _itemCount, + _noMoreItemsIndicatorBuilder, + ); + break; + case PagingStatus.loadingFirstPage: + child = _FirstPageStatusIndicatorBuilder( + builder: _firstPageProgressIndicatorBuilder, + shrinkWrap: _shrinkWrapFirstPageIndicators, + layoutProtocol: _layoutProtocol, + ); + break; + case PagingStatus.subsequentPageError: + child = widget.errorListingBuilder( + context, + (context, index) => _buildListItemWidget( + context, + index, + items!, + ), + _itemCount, + (context) => _newPageErrorIndicatorBuilder(context), + ); + break; + case PagingStatus.noItemsFound: + child = _FirstPageStatusIndicatorBuilder( + builder: _noItemsFoundIndicatorBuilder, + shrinkWrap: _shrinkWrapFirstPageIndicators, + layoutProtocol: _layoutProtocol, + ); + break; + default: + child = _FirstPageStatusIndicatorBuilder( + builder: _firstPageErrorIndicatorBuilder, + shrinkWrap: _shrinkWrapFirstPageIndicators, + layoutProtocol: _layoutProtocol, + ); + } + + if (_builderDelegate.animateTransitions) { + if (_layoutProtocol == PagedLayoutProtocol.sliver) { + return SliverAnimatedSwitcher( + duration: _builderDelegate.transitionDuration, + child: child, + ); + } else { + return AnimatedSwitcher( + duration: _builderDelegate.transitionDuration, + child: child, + ); + } + } else { + return child; + } + } /// Connects the [_pagingController] with the [_builderDelegate] in order to /// create a list item widget and request more items if needed. @@ -269,7 +274,7 @@ class _PagedLayoutBuilderState if (_hasNextPage && isBuildingTriggerIndexItem) { // Schedules the request for the end of this frame. WidgetsBinding.instance.addPostFrameCallback((_) { - _pagingController.notifyPageRequestListeners(_nextKey as PageKeyType); + _fetchNextPage(); }); _hasRequestedNextPage = true; } @@ -280,14 +285,6 @@ class _PagedLayoutBuilderState } } -extension on PagingController { - /// The loaded items count. - int get itemCount => itemList?.length ?? 0; - - /// Tells whether there's a next page to request. - bool get hasNextPage => nextPageKey != null; -} - class _FirstPageStatusIndicatorBuilder extends StatelessWidget { const _FirstPageStatusIndicatorBuilder({ required this.builder, diff --git a/lib/src/widgets/layouts/paged_aligned_grid_view.dart b/lib/src/widgets/layouts/paged_aligned_grid_view.dart index 5180f2e..717a59b 100644 --- a/lib/src/widgets/layouts/paged_aligned_grid_view.dart +++ b/lib/src/widgets/layouts/paged_aligned_grid_view.dart @@ -11,9 +11,11 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; /// from the [flutter_staggered_grid_view](https://pub.dev/packages/flutter_staggered_grid_view) package. /// For more info on how to build staggered grids, check out the /// referred package's documentation and examples. -class PagedAlignedGridView extends BoxScrollView { +class PagedAlignedGridView + extends BoxScrollView { const PagedAlignedGridView({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.gridDelegateBuilder, // Matches [ScrollView.scrollDirection]. @@ -55,7 +57,8 @@ class PagedAlignedGridView extends BoxScrollView { /// Equivalent to [MasonryGridView.count]. PagedAlignedGridView.count({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required int crossAxisCount, super.scrollDirection, @@ -100,7 +103,8 @@ class PagedAlignedGridView extends BoxScrollView { /// Equivalent to [MasonryGridView.extent]. PagedAlignedGridView.extent({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required double maxCrossAxisExtent, super.scrollDirection, @@ -143,8 +147,11 @@ class PagedAlignedGridView extends BoxScrollView { controller: scrollController, ); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -185,7 +192,8 @@ class PagedAlignedGridView extends BoxScrollView { Widget buildChildLayout(BuildContext context) => PagedSliverAlignedGrid( builderDelegate: builderDelegate, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, gridDelegateBuilder: gridDelegateBuilder, mainAxisSpacing: mainAxisSpacing, crossAxisSpacing: crossAxisSpacing, diff --git a/lib/src/widgets/layouts/paged_grid_view.dart b/lib/src/widgets/layouts/paged_grid_view.dart index 467cbc5..8c00584 100644 --- a/lib/src/widgets/layouts/paged_grid_view.dart +++ b/lib/src/widgets/layouts/paged_grid_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_grid.dart'; @@ -8,9 +8,11 @@ import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_grid /// /// Wraps a [PagedSliverGrid] in a [BoxScrollView] so that it can be /// used without the need for a [CustomScrollView]. Similar to a [GridView]. -class PagedGridView extends BoxScrollView { +class PagedGridView + extends BoxScrollView { const PagedGridView({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.gridDelegate, // Matches [ScrollView.controller]. @@ -49,8 +51,11 @@ class PagedGridView extends BoxScrollView { controller: scrollController, ); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -83,7 +88,8 @@ class PagedGridView extends BoxScrollView { Widget buildChildLayout(BuildContext context) => PagedSliverGrid( builderDelegate: builderDelegate, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, gridDelegate: gridDelegate, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, diff --git a/lib/src/widgets/layouts/paged_list_view.dart b/lib/src/widgets/layouts/paged_list_view.dart index 579047f..32aa898 100644 --- a/lib/src/widgets/layouts/paged_list_view.dart +++ b/lib/src/widgets/layouts/paged_list_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_list.dart'; @@ -10,9 +10,11 @@ import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_list /// /// Wraps a [PagedSliverList] in a [BoxScrollView] so that it can be /// used without the need for a [CustomScrollView]. Similar to a [ListView]. -class PagedListView extends BoxScrollView { +class PagedListView + extends BoxScrollView { const PagedListView({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, // Matches [ScrollView.controller]. ScrollController? scrollController, @@ -55,7 +57,8 @@ class PagedListView extends BoxScrollView { ); const PagedListView.separated({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required IndexedWidgetBuilder separatorBuilder, // Matches [ScrollView.controller]. @@ -94,8 +97,11 @@ class PagedListView extends BoxScrollView { controller: scrollController, ); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -131,7 +137,8 @@ class PagedListView extends BoxScrollView { return separatorBuilder != null ? PagedSliverList.separated( builderDelegate: builderDelegate, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, separatorBuilder: separatorBuilder, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, @@ -141,7 +148,8 @@ class PagedListView extends BoxScrollView { ) : PagedSliverList( builderDelegate: builderDelegate, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, addSemanticIndexes: addSemanticIndexes, diff --git a/lib/src/widgets/layouts/paged_masonry_grid_view.dart b/lib/src/widgets/layouts/paged_masonry_grid_view.dart index 1d21ad6..e94d4b2 100644 --- a/lib/src/widgets/layouts/paged_masonry_grid_view.dart +++ b/lib/src/widgets/layouts/paged_masonry_grid_view.dart @@ -11,9 +11,11 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; /// from the [flutter_staggered_grid_view](https://pub.dev/packages/flutter_staggered_grid_view) package. /// For more info on how to build staggered grids, check out the /// referred package's documentation and examples. -class PagedMasonryGridView extends BoxScrollView { +class PagedMasonryGridView + extends BoxScrollView { const PagedMasonryGridView({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.gridDelegateBuilder, // Matches [ScrollView.scrollDirection]. @@ -55,7 +57,8 @@ class PagedMasonryGridView extends BoxScrollView { /// Equivalent to [MasonryGridView.count]. PagedMasonryGridView.count({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required int crossAxisCount, super.scrollDirection, @@ -100,7 +103,8 @@ class PagedMasonryGridView extends BoxScrollView { /// Equivalent to [MasonryGridView.extent]. PagedMasonryGridView.extent({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required double maxCrossAxisExtent, super.scrollDirection, @@ -143,8 +147,11 @@ class PagedMasonryGridView extends BoxScrollView { controller: scrollController, ); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -185,7 +192,8 @@ class PagedMasonryGridView extends BoxScrollView { Widget buildChildLayout(BuildContext context) => PagedSliverMasonryGrid( builderDelegate: builderDelegate, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, gridDelegateBuilder: gridDelegateBuilder, mainAxisSpacing: mainAxisSpacing, crossAxisSpacing: crossAxisSpacing, diff --git a/lib/src/widgets/layouts/paged_page_view.dart b/lib/src/widgets/layouts/paged_page_view.dart index 377e1e6..cfb6a3c 100644 --- a/lib/src/widgets/layouts/paged_page_view.dart +++ b/lib/src/widgets/layouts/paged_page_view.dart @@ -9,9 +9,11 @@ import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_build /// /// Similar to a [PageView]. /// Useful for combining another paged widget with a page view with details. -class PagedPageView extends StatelessWidget { +class PagedPageView + extends StatelessWidget { const PagedPageView({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, @@ -32,8 +34,11 @@ class PagedPageView extends StatelessWidget { super.key, }); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -90,7 +95,8 @@ class PagedPageView extends StatelessWidget { Widget build(BuildContext context) => PagedLayoutBuilder( layoutProtocol: PagedLayoutProtocol.box, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: builderDelegate, shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators, completedListingBuilder: ( diff --git a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart b/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart index 647378f..1b9e43f 100644 --- a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart @@ -12,9 +12,11 @@ import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; /// from the [flutter_staggered_grid_view](https://pub.dev/packages/flutter_staggered_grid_view) package. /// For more info on how to build staggered grids, check out the /// referred package's documentation and examples. -class PagedSliverAlignedGrid extends StatelessWidget { +class PagedSliverAlignedGrid extends StatelessWidget { const PagedSliverAlignedGrid({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.gridDelegateBuilder, this.mainAxisSpacing = 0, @@ -30,7 +32,8 @@ class PagedSliverAlignedGrid extends StatelessWidget { }); PagedSliverAlignedGrid.count({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required int crossAxisCount, this.mainAxisSpacing = 0, @@ -43,14 +46,15 @@ class PagedSliverAlignedGrid extends StatelessWidget { this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, super.key, - }) : gridDelegateBuilder = + }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, )); PagedSliverAlignedGrid.extent({ super.key, - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required double maxCrossAxisExtent, this.mainAxisSpacing = 0, @@ -62,13 +66,16 @@ class PagedSliverAlignedGrid extends StatelessWidget { this.showNewPageErrorIndicatorAsGridChild = true, this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, - }) : gridDelegateBuilder = + }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, )); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -108,7 +115,8 @@ class PagedSliverAlignedGrid extends StatelessWidget { Widget build(BuildContext context) => PagedLayoutBuilder( layoutProtocol: PagedLayoutProtocol.sliver, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: builderDelegate, completedListingBuilder: ( context, diff --git a/lib/src/widgets/layouts/paged_sliver_grid.dart b/lib/src/widgets/layouts/paged_sliver_grid.dart index 5289f9b..53d5e8c 100644 --- a/lib/src/widgets/layouts/paged_sliver_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_grid.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_grid_view.dart'; @@ -12,9 +12,11 @@ import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_grid_view.d /// [CustomScrollView] when added to the screen. /// Useful for combining multiple scrollable pieces in your UI or if you need /// to add some widgets preceding or following your paged grid. -class PagedSliverGrid extends StatelessWidget { +class PagedSliverGrid + extends StatelessWidget { const PagedSliverGrid({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.gridDelegate, this.addAutomaticKeepAlives = true, @@ -27,8 +29,11 @@ class PagedSliverGrid extends StatelessWidget { super.key, }); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -70,7 +75,8 @@ class PagedSliverGrid extends StatelessWidget { Widget build(BuildContext context) => PagedLayoutBuilder( layoutProtocol: PagedLayoutProtocol.sliver, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: builderDelegate, completedListingBuilder: ( context, diff --git a/lib/src/widgets/layouts/paged_sliver_list.dart b/lib/src/widgets/layouts/paged_sliver_list.dart index ef892b8..e7e34fd 100644 --- a/lib/src/widgets/layouts/paged_sliver_list.dart +++ b/lib/src/widgets/layouts/paged_sliver_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_list_view.dart'; @@ -14,9 +14,11 @@ import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_list_view.d /// [CustomScrollView] when added to the screen. /// Useful for combining multiple scrollable pieces in your UI or if you need /// to add some widgets preceding or following your paged list. -class PagedSliverList extends StatelessWidget { +class PagedSliverList + extends StatelessWidget { const PagedSliverList({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, @@ -33,7 +35,8 @@ class PagedSliverList extends StatelessWidget { _separatorBuilder = null; const PagedSliverList.separated({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required IndexedWidgetBuilder separatorBuilder, this.addAutomaticKeepAlives = true, @@ -46,8 +49,11 @@ class PagedSliverList extends StatelessWidget { }) : prototypeItem = null, _separatorBuilder = separatorBuilder; - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -84,7 +90,8 @@ class PagedSliverList extends StatelessWidget { Widget build(BuildContext context) => PagedLayoutBuilder( layoutProtocol: PagedLayoutProtocol.sliver, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: builderDelegate, completedListingBuilder: ( context, @@ -95,7 +102,7 @@ class PagedSliverList extends StatelessWidget { _buildSliverList( itemBuilder, itemCount, - statusIndicatorBuilder: noMoreItemsIndicatorBuilder, + noMoreItemsIndicatorBuilder, ), loadingListingBuilder: ( context, @@ -106,7 +113,7 @@ class PagedSliverList extends StatelessWidget { _buildSliverList( itemBuilder, itemCount, - statusIndicatorBuilder: progressIndicatorBuilder, + progressIndicatorBuilder, ), errorListingBuilder: ( context, @@ -117,16 +124,16 @@ class PagedSliverList extends StatelessWidget { _buildSliverList( itemBuilder, itemCount, - statusIndicatorBuilder: errorIndicatorBuilder, + errorIndicatorBuilder, ), shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators, ); SliverMultiBoxAdaptorWidget _buildSliverList( IndexedWidgetBuilder itemBuilder, - int itemCount, { + int itemCount, WidgetBuilder? statusIndicatorBuilder, - }) { + ) { final delegate = _buildSliverDelegate( itemBuilder, itemCount, diff --git a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart b/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart index 53881a4..c99fd50 100644 --- a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart @@ -16,9 +16,11 @@ typedef SliverSimpleGridDelegateBuilder = SliverSimpleGridDelegate Function( /// from the [flutter_staggered_grid_view](https://pub.dev/packages/flutter_staggered_grid_view) package. /// For more info on how to build staggered grids, check out the /// referred package's documentation and examples. -class PagedSliverMasonryGrid extends StatelessWidget { +class PagedSliverMasonryGrid extends StatelessWidget { const PagedSliverMasonryGrid({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required this.gridDelegateBuilder, this.mainAxisSpacing = 0, @@ -35,7 +37,8 @@ class PagedSliverMasonryGrid extends StatelessWidget { /// Equivalent to [SliverMasonryGrid.count]. PagedSliverMasonryGrid.count({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required int crossAxisCount, this.mainAxisSpacing = 0, @@ -48,14 +51,15 @@ class PagedSliverMasonryGrid extends StatelessWidget { this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, super.key, - }) : gridDelegateBuilder = + }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, )); /// Equivalent to [SliverMasonryGrid.extent]. PagedSliverMasonryGrid.extent({ - required this.pagingController, + required this.state, + required this.fetchNextPage, required this.builderDelegate, required double maxCrossAxisExtent, this.mainAxisSpacing = 0, @@ -68,13 +72,16 @@ class PagedSliverMasonryGrid extends StatelessWidget { this.showNoMoreItemsIndicatorAsGridChild = true, this.shrinkWrapFirstPageIndicators = false, super.key, - }) : gridDelegateBuilder = + }) : gridDelegateBuilder = ((childCount) => SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, )); - /// Matches [PagedLayoutBuilder.pagingController]. - final PagingController pagingController; + /// Matches [PagedLayoutBuilder.state]. + final PagingState state; + + /// Matches [PagedLayoutBuilder.onPageRequest]. + final NextPageCallback fetchNextPage; /// Matches [PagedLayoutBuilder.builderDelegate]. final PagedChildBuilderDelegate builderDelegate; @@ -114,7 +121,8 @@ class PagedSliverMasonryGrid extends StatelessWidget { Widget build(BuildContext context) => PagedLayoutBuilder( layoutProtocol: PagedLayoutProtocol.sliver, - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: builderDelegate, completedListingBuilder: ( context, diff --git a/lib/src/widgets/state/paging_listener.dart b/lib/src/widgets/state/paging_listener.dart new file mode 100644 index 0000000..6b7860d --- /dev/null +++ b/lib/src/widgets/state/paging_listener.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; + +import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; + +class PagingListener + extends StatelessWidget { + const PagingListener({ + super.key, + required this.controller, + required this.builder, + }); + + final PagingController controller; + final Widget Function( + BuildContext context, + PagingState state, + NextPageCallback fetchNextPage, + ) builder; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: controller, + builder: (context, state, _) => builder( + context, + state, + controller.fetchNextPage, + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 72f4ef7..8624398 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 4.1.0 homepage: https://github.com/EdsonBueno/infinite_scroll_pagination environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.4.0 <4.0.0" flutter: ">=3.0.0" dependencies: From 9b7a386732780eee276ae51ee5205fe3373c51b2 Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 21 Oct 2024 14:41:59 +0200 Subject: [PATCH 05/37] refactor: example search and grid --- example/lib/common/listing_bloc.dart | 3 ++ example/lib/common/search_input.dart | 59 ++++++++++++++++++++-------- example/lib/remote/api.dart | 21 +++++----- example/lib/samples/sliver_grid.dart | 6 +++ 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/example/lib/common/listing_bloc.dart b/example/lib/common/listing_bloc.dart index 0660776..c57de2c 100644 --- a/example/lib/common/listing_bloc.dart +++ b/example/lib/common/listing_bloc.dart @@ -13,6 +13,7 @@ class PhotoPagesBloc { .addTo(_subscriptions); _onSearchChanged.stream + .distinct() .flatMap((_) => _refresh()) .listen(_stateController.add) .addTo(_subscriptions); @@ -57,6 +58,7 @@ class PhotoPagesBloc { final isLastPage = newItems.isEmpty; yield lastListingState.copyWith( error: null, + isLoading: false, hasNextPage: !isLastPage, pages: [ ...lastListingState.pages ?? [], @@ -70,6 +72,7 @@ class PhotoPagesBloc { } catch (e) { yield lastListingState.copyWith( error: e, + isLoading: false, ); } } diff --git a/example/lib/common/search_input.dart b/example/lib/common/search_input.dart index c09d315..25d0ad8 100644 --- a/example/lib/common/search_input.dart +++ b/example/lib/common/search_input.dart @@ -8,9 +8,12 @@ class SearchInputSliver extends StatefulWidget { super.key, this.onChanged, this.debounceTime, + required this.getSuggestions, }); + final ValueChanged? onChanged; final Duration? debounceTime; + final List Function(String) getSuggestions; @override State createState() => _SearchInputSliverState(); @@ -21,6 +24,8 @@ class _SearchInputSliverState extends State { StreamController(); late StreamSubscription _textChangesSubscription; + final SearchController _searchController = SearchController(); + @override void initState() { super.initState(); @@ -32,28 +37,50 @@ class _SearchInputSliverState extends State { ), ) .distinct() - .listen((text) { - final onChanged = widget.onChanged; - if (onChanged != null) { - onChanged(text); - } - }); + .listen(widget.onChanged?.call); } @override Widget build(BuildContext context) => SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all( - 16, - ), - child: TextField( - decoration: const InputDecoration( - prefixIcon: Icon( - Icons.search, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8), + child: SearchAnchor( + searchController: _searchController, + viewOnSubmitted: (value) { + widget.onChanged?.call(value); + _searchController.closeView(value); + }, + suggestionsBuilder: (context, controller) => + widget.getSuggestions(controller.text).map( + (suggestion) => ListTile( + title: Text(suggestion), + onTap: () { + controller.closeView(suggestion); + widget.onChanged?.call(suggestion); + }, + ), + ), + viewShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + builder: (context, controller) => SearchBar( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + controller: controller, + hintText: 'Search...', + onTap: () { + controller.openView(); + }, + onChanged: (_) { + controller.openView(); + }, + leading: const Icon(Icons.search), ), - hintText: 'Search...', ), - onChanged: _textChangeStreamController.add, ), ), ); diff --git a/example/lib/remote/api.dart b/example/lib/remote/api.dart index b84e38d..120dc72 100644 --- a/example/lib/remote/api.dart +++ b/example/lib/remote/api.dart @@ -16,16 +16,19 @@ class RemoteApi { throw RandomChanceException(); } - return http - .get( - _ApiUrlBuilder.photos(page, limit, search), - ) - .mapFromResponse, List>( - (jsonArray) => _parseItemListFromJsonArray( - jsonArray, - Photo.fromPlaceholderJson, + return Future.delayed( + const Duration(seconds: 0), + () => http + .get( + _ApiUrlBuilder.photos(page, limit, search), + ) + .mapFromResponse, List>( + (jsonArray) => _parseItemListFromJsonArray( + jsonArray, + Photo.fromPlaceholderJson, + ), ), - ); + ); } static List _parseItemListFromJsonArray( diff --git a/example/lib/samples/sliver_grid.dart b/example/lib/samples/sliver_grid.dart index 4073893..a1e0606 100644 --- a/example/lib/samples/sliver_grid.dart +++ b/example/lib/samples/sliver_grid.dart @@ -50,6 +50,12 @@ class _SliverGridScreenState extends State { onChanged: (searchTerm) => _bloc.changeSearch( searchTerm, ), + getSuggestions: (searchTerm) => (_bloc.state.items + ?.expand((photo) => photo.title.split(' ')) + .where((e) => e.contains(searchTerm)) + .toSet() + .toList() ?? + []), ), SliverToBoxAdapter( child: Padding( From f876fa883919c1217594fe47be94f745639d4c91 Mon Sep 17 00:00:00 2001 From: clragon Date: Tue, 22 Oct 2024 01:19:12 +0200 Subject: [PATCH 06/37] fix: library imports --- lib/src/model/paging_status.dart | 2 +- lib/src/widgets/helpers/paged_layout_builder.dart | 5 ++++- lib/src/widgets/layouts/paged_aligned_grid_view.dart | 6 +++++- lib/src/widgets/layouts/paged_masonry_grid_view.dart | 5 ++++- lib/src/widgets/layouts/paged_page_view.dart | 4 +++- lib/src/widgets/layouts/paged_sliver_aligned_grid.dart | 5 ++++- lib/src/widgets/layouts/paged_sliver_masonry_grid.dart | 4 +++- 7 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/src/model/paging_status.dart b/lib/src/model/paging_status.dart index abe17bd..8dd1370 100644 --- a/lib/src/model/paging_status.dart +++ b/lib/src/model/paging_status.dart @@ -1,4 +1,4 @@ -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; /// All possible status for a pagination. enum PagingStatus { diff --git a/lib/src/widgets/helpers/paged_layout_builder.dart b/lib/src/widgets/helpers/paged_layout_builder.dart index 8bc0df6..f3f7b66 100644 --- a/lib/src/widgets/helpers/paged_layout_builder.dart +++ b/lib/src/widgets/helpers/paged_layout_builder.dart @@ -2,7 +2,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_status.dart'; + import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart'; import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart'; diff --git a/lib/src/widgets/layouts/paged_aligned_grid_view.dart b/lib/src/widgets/layouts/paged_aligned_grid_view.dart index 717a59b..f360fe3 100644 --- a/lib/src/widgets/layouts/paged_aligned_grid_view.dart +++ b/lib/src/widgets/layouts/paged_aligned_grid_view.dart @@ -1,6 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_aligned_grid.dart'; +import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_masonry_grid.dart'; /// A [AlignedGridView] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_masonry_grid_view.dart b/lib/src/widgets/layouts/paged_masonry_grid_view.dart index e94d4b2..268c099 100644 --- a/lib/src/widgets/layouts/paged_masonry_grid_view.dart +++ b/lib/src/widgets/layouts/paged_masonry_grid_view.dart @@ -1,6 +1,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_masonry_grid.dart'; /// A [MasonryGridView] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_page_view.dart b/lib/src/widgets/layouts/paged_page_view.dart index cfb6a3c..7759507 100644 --- a/lib/src/widgets/layouts/paged_page_view.dart +++ b/lib/src/widgets/layouts/paged_page_view.dart @@ -1,8 +1,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; /// Paged [PageView] with progress and error indicators displayed as the last /// item. diff --git a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart b/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart index 1b9e43f..abe11ce 100644 --- a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart @@ -1,7 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; +import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_masonry_grid.dart'; /// A [SliverAlignedGrid] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart b/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart index c99fd50..1fa597b 100644 --- a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart +++ b/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart @@ -1,7 +1,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; +import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; typedef SliverSimpleGridDelegateBuilder = SliverSimpleGridDelegate Function( int childCount, From e3a0156b29eca4b807bde9c6760d6ec9d93be009 Mon Sep 17 00:00:00 2001 From: clragon Date: Tue, 22 Oct 2024 13:56:48 +0200 Subject: [PATCH 07/37] refactor: make paging controller mutex public --- lib/src/core/paging_controller.dart | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index 95b0f63..9f735e7 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -38,17 +38,19 @@ class PagingController /// Keeps track of the current operation. /// If the operation changes during its execution, the operation is cancelled. /// - /// In more concrete terms, this cancels old fetch calls. - Object? _operation; + /// Instead of using this property directly, use [fetchNextPage], [refresh], or [cancel]. + /// If you are extending this class, check and set this property before and after the fetch operation. + @protected + Object? operation; /// Fetches the next page. /// /// If called while a page is fetching or no more pages are available, this method does nothing. void fetchNextPage() async { // We are already loading a new page. - if (_operation != null) return; + if (this.operation != null) return; - final operation = _operation = Object(); + final operation = this.operation = Object(); // we use a local copy of value, // so that we only send one notification now and at the end of the method. @@ -79,7 +81,7 @@ class PagingController newItems = fetchResult; } - if (operation != _operation) return; + if (operation != operation) return; state = state.copyWith( pages: [...?state.pages, newItems], @@ -96,8 +98,8 @@ class PagingController } } finally { value = state.copyWith(isLoading: false); - if (operation == _operation) { - _operation = null; + if (operation == this.operation) { + this.operation = null; } } } @@ -106,7 +108,7 @@ class PagingController /// /// This cancels the current fetch operation and resets the state. void refresh() { - _operation = null; + operation = null; value = value.reset(); } @@ -114,7 +116,7 @@ class PagingController /// /// This can be called right before a call to [fetchNextPage] to force a new fetch. void cancel() { - _operation = null; + operation = null; value = value.copyWith(isLoading: false); } } From f9ec432230b5e52469f70e32e8532d6293bc205b Mon Sep 17 00:00:00 2001 From: clragon Date: Tue, 22 Oct 2024 14:19:18 +0200 Subject: [PATCH 08/37] refactor: folder structure --- lib/infinite_scroll_pagination.dart | 22 +++++++++---------- .../first_page_error_indicator.dart | 2 +- .../first_page_exception_indicator.dart | 0 .../first_page_progress_indicator.dart | 0 .../footer_tile.dart | 0 .../new_page_error_indicator.dart | 2 +- .../new_page_progress_indicator.dart | 2 +- .../no_items_found_indicator.dart | 2 +- .../paged_layout_builder.dart | 10 ++++----- .../state => base}/paging_listener.dart | 2 +- .../layouts/paged_aligned_grid_view.dart | 6 ++--- .../layouts/paged_grid_view.dart | 4 ++-- .../layouts/paged_list_view.dart | 4 ++-- .../layouts/paged_masonry_grid_view.dart | 4 ++-- .../layouts/paged_page_view.dart | 2 +- .../layouts/paged_sliver_aligned_grid.dart | 4 ++-- .../layouts/paged_sliver_grid.dart | 4 ++-- .../layouts/paged_sliver_list.dart | 4 ++-- .../layouts/paged_sliver_masonry_grid.dart | 2 +- test/paged_layout_builder_test.dart | 10 ++++----- 20 files changed, 43 insertions(+), 43 deletions(-) rename lib/src/{widgets/helpers => base}/default_status_indicators/first_page_error_indicator.dart (79%) rename lib/src/{widgets/helpers => base}/default_status_indicators/first_page_exception_indicator.dart (100%) rename lib/src/{widgets/helpers => base}/default_status_indicators/first_page_progress_indicator.dart (100%) rename lib/src/{widgets/helpers => base}/default_status_indicators/footer_tile.dart (100%) rename lib/src/{widgets/helpers => base}/default_status_indicators/new_page_error_indicator.dart (88%) rename lib/src/{widgets/helpers => base}/default_status_indicators/new_page_progress_indicator.dart (71%) rename lib/src/{widgets/helpers => base}/default_status_indicators/no_items_found_indicator.dart (73%) rename lib/src/{widgets/helpers => base}/paged_layout_builder.dart (94%) rename lib/src/{widgets/state => base}/paging_listener.dart (90%) rename lib/src/{widgets => }/layouts/paged_aligned_grid_view.dart (96%) rename lib/src/{widgets => }/layouts/paged_grid_view.dart (95%) rename lib/src/{widgets => }/layouts/paged_list_view.dart (96%) rename lib/src/{widgets => }/layouts/paged_masonry_grid_view.dart (97%) rename lib/src/{widgets => }/layouts/paged_page_view.dart (98%) rename lib/src/{widgets => }/layouts/paged_sliver_aligned_grid.dart (97%) rename lib/src/{widgets => }/layouts/paged_sliver_grid.dart (96%) rename lib/src/{widgets => }/layouts/paged_sliver_list.dart (97%) rename lib/src/{widgets => }/layouts/paged_sliver_masonry_grid.dart (98%) diff --git a/lib/infinite_scroll_pagination.dart b/lib/infinite_scroll_pagination.dart index 7fb2734..aba41e7 100644 --- a/lib/infinite_scroll_pagination.dart +++ b/lib/infinite_scroll_pagination.dart @@ -2,14 +2,14 @@ export 'src/core/paged_child_builder_delegate.dart'; export 'src/core/paging_controller.dart'; export 'src/model/paging_state.dart'; export 'src/model/paging_status.dart'; -export 'src/widgets/helpers/paged_layout_builder.dart'; -export 'src/widgets/layouts/paged_grid_view.dart'; -export 'src/widgets/layouts/paged_list_view.dart'; -export 'src/widgets/layouts/paged_masonry_grid_view.dart'; -export 'src/widgets/layouts/paged_aligned_grid_view.dart'; -export 'src/widgets/layouts/paged_page_view.dart'; -export 'src/widgets/layouts/paged_sliver_grid.dart'; -export 'src/widgets/layouts/paged_sliver_list.dart'; -export 'src/widgets/layouts/paged_sliver_masonry_grid.dart'; -export 'src/widgets/layouts/paged_sliver_aligned_grid.dart'; -export 'src/widgets/state/paging_listener.dart'; +export 'src/base/paged_layout_builder.dart'; +export 'src/layouts/paged_grid_view.dart'; +export 'src/layouts/paged_list_view.dart'; +export 'src/layouts/paged_masonry_grid_view.dart'; +export 'src/layouts/paged_aligned_grid_view.dart'; +export 'src/layouts/paged_page_view.dart'; +export 'src/layouts/paged_sliver_grid.dart'; +export 'src/layouts/paged_sliver_list.dart'; +export 'src/layouts/paged_sliver_masonry_grid.dart'; +export 'src/layouts/paged_sliver_aligned_grid.dart'; +export 'src/base/paging_listener.dart'; diff --git a/lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart b/lib/src/base/default_status_indicators/first_page_error_indicator.dart similarity index 79% rename from lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart rename to lib/src/base/default_status_indicators/first_page_error_indicator.dart index ea8848c..fd83e1e 100644 --- a/lib/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart +++ b/lib/src/base/default_status_indicators/first_page_error_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_exception_indicator.dart'; class FirstPageErrorIndicator extends StatelessWidget { const FirstPageErrorIndicator({ diff --git a/lib/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart b/lib/src/base/default_status_indicators/first_page_exception_indicator.dart similarity index 100% rename from lib/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart rename to lib/src/base/default_status_indicators/first_page_exception_indicator.dart diff --git a/lib/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart b/lib/src/base/default_status_indicators/first_page_progress_indicator.dart similarity index 100% rename from lib/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart rename to lib/src/base/default_status_indicators/first_page_progress_indicator.dart diff --git a/lib/src/widgets/helpers/default_status_indicators/footer_tile.dart b/lib/src/base/default_status_indicators/footer_tile.dart similarity index 100% rename from lib/src/widgets/helpers/default_status_indicators/footer_tile.dart rename to lib/src/base/default_status_indicators/footer_tile.dart diff --git a/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart b/lib/src/base/default_status_indicators/new_page_error_indicator.dart similarity index 88% rename from lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart rename to lib/src/base/default_status_indicators/new_page_error_indicator.dart index 4bb77c8..94a2fa4 100644 --- a/lib/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart +++ b/lib/src/base/default_status_indicators/new_page_error_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/footer_tile.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/footer_tile.dart'; class NewPageErrorIndicator extends StatelessWidget { const NewPageErrorIndicator({ diff --git a/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart b/lib/src/base/default_status_indicators/new_page_progress_indicator.dart similarity index 71% rename from lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart rename to lib/src/base/default_status_indicators/new_page_progress_indicator.dart index 920ea5a..870d1bb 100644 --- a/lib/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart +++ b/lib/src/base/default_status_indicators/new_page_progress_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/footer_tile.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/footer_tile.dart'; class NewPageProgressIndicator extends StatelessWidget { const NewPageProgressIndicator({super.key}); diff --git a/lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart b/lib/src/base/default_status_indicators/no_items_found_indicator.dart similarity index 73% rename from lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart rename to lib/src/base/default_status_indicators/no_items_found_indicator.dart index e9786d6..9d9c1ff 100644 --- a/lib/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart +++ b/lib/src/base/default_status_indicators/no_items_found_indicator.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_exception_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_exception_indicator.dart'; class NoItemsFoundIndicator extends StatelessWidget { const NoItemsFoundIndicator({super.key}); diff --git a/lib/src/widgets/helpers/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart similarity index 94% rename from lib/src/widgets/helpers/paged_layout_builder.dart rename to lib/src/base/paged_layout_builder.dart index f3f7b66..6b568ed 100644 --- a/lib/src/widgets/helpers/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -6,11 +6,11 @@ import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/model/paging_status.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/no_items_found_indicator.dart'; import 'package:sliver_tools/sliver_tools.dart'; /// Called to request a new page of data. diff --git a/lib/src/widgets/state/paging_listener.dart b/lib/src/base/paging_listener.dart similarity index 90% rename from lib/src/widgets/state/paging_listener.dart rename to lib/src/base/paging_listener.dart index 6b7860d..1fa3e98 100644 --- a/lib/src/widgets/state/paging_listener.dart +++ b/lib/src/base/paging_listener.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; class PagingListener extends StatelessWidget { diff --git a/lib/src/widgets/layouts/paged_aligned_grid_view.dart b/lib/src/layouts/paged_aligned_grid_view.dart similarity index 96% rename from lib/src/widgets/layouts/paged_aligned_grid_view.dart rename to lib/src/layouts/paged_aligned_grid_view.dart index f360fe3..65cce19 100644 --- a/lib/src/widgets/layouts/paged_aligned_grid_view.dart +++ b/lib/src/layouts/paged_aligned_grid_view.dart @@ -2,9 +2,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_aligned_grid.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_masonry_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_aligned_grid.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; /// A [AlignedGridView] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_grid_view.dart b/lib/src/layouts/paged_grid_view.dart similarity index 95% rename from lib/src/widgets/layouts/paged_grid_view.dart rename to lib/src/layouts/paged_grid_view.dart index 8c00584..afb5cb2 100644 --- a/lib/src/widgets/layouts/paged_grid_view.dart +++ b/lib/src/layouts/paged_grid_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_grid.dart'; /// A [GridView] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_list_view.dart b/lib/src/layouts/paged_list_view.dart similarity index 96% rename from lib/src/widgets/layouts/paged_list_view.dart rename to lib/src/layouts/paged_list_view.dart index 32aa898..6ee485c 100644 --- a/lib/src/widgets/layouts/paged_list_view.dart +++ b/lib/src/layouts/paged_list_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_list.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_list.dart'; /// A [ListView] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_masonry_grid_view.dart b/lib/src/layouts/paged_masonry_grid_view.dart similarity index 97% rename from lib/src/widgets/layouts/paged_masonry_grid_view.dart rename to lib/src/layouts/paged_masonry_grid_view.dart index 268c099..15579b1 100644 --- a/lib/src/widgets/layouts/paged_masonry_grid_view.dart +++ b/lib/src/layouts/paged_masonry_grid_view.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_masonry_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; /// A [MasonryGridView] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_page_view.dart b/lib/src/layouts/paged_page_view.dart similarity index 98% rename from lib/src/widgets/layouts/paged_page_view.dart rename to lib/src/layouts/paged_page_view.dart index 7759507..ec3226e 100644 --- a/lib/src/widgets/layouts/paged_page_view.dart +++ b/lib/src/layouts/paged_page_view.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; /// Paged [PageView] with progress and error indicators displayed as the last /// item. diff --git a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart b/lib/src/layouts/paged_sliver_aligned_grid.dart similarity index 97% rename from lib/src/widgets/layouts/paged_sliver_aligned_grid.dart rename to lib/src/layouts/paged_sliver_aligned_grid.dart index abe11ce..0e60da6 100644 --- a/lib/src/widgets/layouts/paged_sliver_aligned_grid.dart +++ b/lib/src/layouts/paged_sliver_aligned_grid.dart @@ -3,8 +3,8 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_sliver_masonry_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; /// A [SliverAlignedGrid] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_sliver_grid.dart b/lib/src/layouts/paged_sliver_grid.dart similarity index 96% rename from lib/src/widgets/layouts/paged_sliver_grid.dart rename to lib/src/layouts/paged_sliver_grid.dart index 53d5e8c..5e8300c 100644 --- a/lib/src/widgets/layouts/paged_sliver_grid.dart +++ b/lib/src/layouts/paged_sliver_grid.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_grid_view.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_grid_view.dart'; /// Paged [SliverGrid] with progress and error indicators displayed as the last /// item. diff --git a/lib/src/widgets/layouts/paged_sliver_list.dart b/lib/src/layouts/paged_sliver_list.dart similarity index 97% rename from lib/src/widgets/layouts/paged_sliver_list.dart rename to lib/src/layouts/paged_sliver_list.dart index e7e34fd..815d50c 100644 --- a/lib/src/widgets/layouts/paged_sliver_list.dart +++ b/lib/src/layouts/paged_sliver_list.dart @@ -3,8 +3,8 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/widgets/layouts/paged_list_view.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/layouts/paged_list_view.dart'; /// A [SliverList] with pagination capabilities. /// diff --git a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart b/lib/src/layouts/paged_sliver_masonry_grid.dart similarity index 98% rename from lib/src/widgets/layouts/paged_sliver_masonry_grid.dart rename to lib/src/layouts/paged_sliver_masonry_grid.dart index 1fa597b..e045661 100644 --- a/lib/src/widgets/layouts/paged_sliver_masonry_grid.dart +++ b/lib/src/layouts/paged_sliver_masonry_grid.dart @@ -3,7 +3,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; typedef SliverSimpleGridDelegateBuilder = SliverSimpleGridDelegate Function( int childCount, diff --git a/test/paged_layout_builder_test.dart b/test/paged_layout_builder_test.dart index 636fab2..93a3178 100644 --- a/test/paged_layout_builder_test.dart +++ b/test/paged_layout_builder_test.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/first_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/new_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/new_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/widgets/helpers/default_status_indicators/no_items_found_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/base/default_status_indicators/no_items_found_indicator.dart'; import 'utils/paging_controller_utils.dart'; From df577fed4d2e539b458dd0e9b856419927fe16b8 Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 23 Oct 2024 01:28:33 +0200 Subject: [PATCH 09/37] refactor: state status logic --- lib/src/base/paged_layout_builder.dart | 53 +++++++++++++------------- lib/src/model/paging_status.dart | 34 ++++++----------- 2 files changed, 38 insertions(+), 49 deletions(-) diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index 6b568ed..b9b255e 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -182,6 +182,27 @@ class _PagedLayoutBuilderState _buildListItemWidget( context, @@ -207,18 +228,11 @@ class _PagedLayoutBuilderState _newPageErrorIndicatorBuilder(context), ); break; - case PagingStatus.subsequentPageError: - child = widget.errorListingBuilder( + case PagingStatus.completed: + child = widget.completedListingBuilder( context, (context, index) => _buildListItemWidget( context, @@ -226,22 +240,9 @@ class _PagedLayoutBuilderState _newPageErrorIndicatorBuilder(context), - ); - break; - case PagingStatus.noItemsFound: - child = _FirstPageStatusIndicatorBuilder( - builder: _noItemsFoundIndicatorBuilder, - shrinkWrap: _shrinkWrapFirstPageIndicators, - layoutProtocol: _layoutProtocol, + _noMoreItemsIndicatorBuilder, ); break; - default: - child = _FirstPageStatusIndicatorBuilder( - builder: _firstPageErrorIndicatorBuilder, - shrinkWrap: _shrinkWrapFirstPageIndicators, - layoutProtocol: _layoutProtocol, - ); } if (_builderDelegate.animateTransitions) { diff --git a/lib/src/model/paging_status.dart b/lib/src/model/paging_status.dart index 8dd1370..31e093e 100644 --- a/lib/src/model/paging_status.dart +++ b/lib/src/model/paging_status.dart @@ -21,40 +21,28 @@ extension PagingStatusExtension on PagingState { bool get _hasError => error != null; + bool get _isLoadingFirstPage => _itemCount == null && !_hasError; + + bool get _hasFirstPageError => !_hasItems && _hasError; + bool get _isListingUnfinished => _hasItems && hasNextPage; bool get _isOngoing => _isListingUnfinished && !_hasError; bool get _isCompleted => _hasItems && !hasNextPage; - bool get _isLoadingFirstPage => _itemCount == null && !_hasError; - bool get _hasSubsequentPageError => _isListingUnfinished && _hasError; bool get _isEmpty => _itemCount != null && _itemCount == 0; /// The current pagination status. PagingStatus get status { - if (_isOngoing) { - return PagingStatus.ongoing; - } - - if (_isCompleted) { - return PagingStatus.completed; - } - - if (_isLoadingFirstPage) { - return PagingStatus.loadingFirstPage; - } - - if (_hasSubsequentPageError) { - return PagingStatus.subsequentPageError; - } - - if (_isEmpty) { - return PagingStatus.noItemsFound; - } else { - return PagingStatus.firstPageError; - } + if (_isLoadingFirstPage) return PagingStatus.loadingFirstPage; + if (_hasFirstPageError) return PagingStatus.firstPageError; + if (_isEmpty) return PagingStatus.noItemsFound; + if (_isOngoing) return PagingStatus.ongoing; + if (_hasSubsequentPageError) return PagingStatus.subsequentPageError; + if (_isCompleted) return PagingStatus.completed; + throw StateError('Unknown status; Did you forget to implement a case?'); } } From 462f8c08946bddd63df9e1cc2bc6778442f73c17 Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 23 Oct 2024 01:38:40 +0200 Subject: [PATCH 10/37] refactor: simplify paged layout builder build method --- lib/src/base/paged_layout_builder.dart | 184 ++++++++++++------------- 1 file changed, 89 insertions(+), 95 deletions(-) diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index b9b255e..2df89b6 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -179,87 +179,62 @@ class _PagedLayoutBuilderState _buildListItemWidget( + return _PagedLayoutAnimator( + animateTransitions: _builderDelegate.animateTransitions, + transitionDuration: _builderDelegate.transitionDuration, + layoutProtocol: _layoutProtocol, + child: switch (_state.status) { + PagingStatus.loadingFirstPage => _FirstPageStatusIndicatorBuilder( + builder: _firstPageProgressIndicatorBuilder, + shrinkWrap: _shrinkWrapFirstPageIndicators, + layoutProtocol: _layoutProtocol, + ), + PagingStatus.firstPageError => _FirstPageStatusIndicatorBuilder( + builder: _firstPageErrorIndicatorBuilder, + shrinkWrap: _shrinkWrapFirstPageIndicators, + layoutProtocol: _layoutProtocol, + ), + PagingStatus.noItemsFound => _FirstPageStatusIndicatorBuilder( + builder: _noItemsFoundIndicatorBuilder, + shrinkWrap: _shrinkWrapFirstPageIndicators, + layoutProtocol: _layoutProtocol, + ), + PagingStatus.ongoing => widget.loadingListingBuilder( context, - index, - items!, + // We must create this closure to close over the [itemList] + // value. That way, we are safe if [itemList] value changes + // while Flutter rebuilds the widget (due to animations, for + // example.) + (context, index) => _buildListItemWidget( + context, + index, + _state.items!, + ), + _itemCount, + _newPageProgressIndicatorBuilder, ), - _itemCount, - _newPageProgressIndicatorBuilder, - ); - break; - case PagingStatus.subsequentPageError: - child = widget.errorListingBuilder( - context, - (context, index) => _buildListItemWidget( + PagingStatus.subsequentPageError => widget.errorListingBuilder( context, - index, - items!, + (context, index) => _buildListItemWidget( + context, + index, + _state.items!, + ), + _itemCount, + (context) => _newPageErrorIndicatorBuilder(context), ), - _itemCount, - (context) => _newPageErrorIndicatorBuilder(context), - ); - break; - case PagingStatus.completed: - child = widget.completedListingBuilder( - context, - (context, index) => _buildListItemWidget( + PagingStatus.completed => widget.completedListingBuilder( context, - index, - items!, + (context, index) => _buildListItemWidget( + context, + index, + _state.items!, + ), + _itemCount, + _noMoreItemsIndicatorBuilder, ), - _itemCount, - _noMoreItemsIndicatorBuilder, - ); - break; - } - - if (_builderDelegate.animateTransitions) { - if (_layoutProtocol == PagedLayoutProtocol.sliver) { - return SliverAnimatedSwitcher( - duration: _builderDelegate.transitionDuration, - child: child, - ); - } else { - return AnimatedSwitcher( - duration: _builderDelegate.transitionDuration, - child: child, - ); - } - } else { - return child; - } + }, + ); } /// Connects the [_pagingController] with the [_builderDelegate] in order to @@ -289,6 +264,35 @@ class _PagedLayoutBuilderState SliverAnimatedSwitcher( + duration: transitionDuration, + child: child, + ), + PagedLayoutProtocol.box => AnimatedSwitcher( + duration: transitionDuration, + child: child, + ), + }; + } +} + class _FirstPageStatusIndicatorBuilder extends StatelessWidget { const _FirstPageStatusIndicatorBuilder({ required this.builder, @@ -302,25 +306,15 @@ class _FirstPageStatusIndicatorBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - if (layoutProtocol == PagedLayoutProtocol.sliver) { - if (shrinkWrap) { - return SliverToBoxAdapter( - child: builder(context), - ); - } else { - return SliverFillRemaining( - hasScrollBody: false, - child: builder(context), - ); - } - } else { - if (shrinkWrap) { - return builder(context); - } else { - return Center( - child: builder(context), - ); - } - } + return switch (layoutProtocol) { + PagedLayoutProtocol.sliver => shrinkWrap + ? SliverToBoxAdapter(child: builder(context)) + : SliverFillRemaining( + hasScrollBody: false, + child: builder(context), + ), + PagedLayoutProtocol.box => + shrinkWrap ? builder(context) : Center(child: builder(context)), + }; } } From 83ac28a0a69dc39b745e4e4ed9f7e1eb50d5ef5d Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 13 Dec 2024 03:13:01 +0100 Subject: [PATCH 11/37] refactor: tests for new logic --- lib/infinite_scroll_pagination.dart | 8 +- .../paged_child_builder_delegate.dart | 0 lib/src/base/paged_layout_builder.dart | 6 +- lib/src/base/paging_listener.dart | 2 +- lib/src/core/paging_controller.dart | 11 +- lib/src/{model => core}/paging_state.dart | 9 +- .../{model => core}/paging_state_base.dart | 21 +- lib/src/{model => core}/paging_status.dart | 4 +- ...ppended_sliver_child_builder_delegate.dart | 0 .../appended_sliver_grid.dart | 0 .../listenable_listener.dart | 0 lib/src/layouts/paged_aligned_grid_view.dart | 4 +- lib/src/layouts/paged_grid_view.dart | 4 +- lib/src/layouts/paged_list_view.dart | 4 +- lib/src/layouts/paged_masonry_grid_view.dart | 4 +- lib/src/layouts/paged_page_view.dart | 6 +- .../layouts/paged_sliver_aligned_grid.dart | 6 +- lib/src/layouts/paged_sliver_grid.dart | 6 +- lib/src/layouts/paged_sliver_list.dart | 6 +- .../layouts/paged_sliver_masonry_grid.dart | 6 +- pubspec.yaml | 1 + .../{ => base}/paged_layout_builder_test.dart | 76 ++--- test/core/paging_controller_test.dart | 154 +++++++++ test/core/paging_state_base_test.dart | 213 ++++++++++++ test/core/paging_status_test.dart | 69 ++++ test/{ => layouts}/paged_grid_view_test.dart | 125 +++---- test/{ => layouts}/paged_list_view_test.dart | 112 ++----- .../paged_masonry_grid_view_test.dart | 96 ++---- test/{ => layouts}/paged_page_view_test.dart | 68 +--- .../{ => layouts}/paged_sliver_list_test.dart | 112 ++----- test/paging_controller_test.dart | 315 ------------------ test/paging_state_test.dart | 128 ------- test/utils/paging_controller_utils.dart | 114 +++---- 33 files changed, 741 insertions(+), 949 deletions(-) rename lib/src/{core => base}/paged_child_builder_delegate.dart (100%) rename lib/src/{model => core}/paging_state.dart (91%) rename lib/src/{model => core}/paging_state_base.dart (82%) rename lib/src/{model => core}/paging_status.dart (91%) rename lib/src/{utils => helpers}/appended_sliver_child_builder_delegate.dart (100%) rename lib/src/{utils => helpers}/appended_sliver_grid.dart (100%) rename lib/src/{utils => helpers}/listenable_listener.dart (100%) rename test/{ => base}/paged_layout_builder_test.dart (86%) create mode 100644 test/core/paging_controller_test.dart create mode 100644 test/core/paging_state_base_test.dart create mode 100644 test/core/paging_status_test.dart rename test/{ => layouts}/paged_grid_view_test.dart (70%) rename test/{ => layouts}/paged_list_view_test.dart (67%) rename test/{ => layouts}/paged_masonry_grid_view_test.dart (66%) rename test/{ => layouts}/paged_page_view_test.dart (63%) rename test/{ => layouts}/paged_sliver_list_test.dart (67%) delete mode 100644 test/paging_controller_test.dart delete mode 100644 test/paging_state_test.dart diff --git a/lib/infinite_scroll_pagination.dart b/lib/infinite_scroll_pagination.dart index aba41e7..e3baa22 100644 --- a/lib/infinite_scroll_pagination.dart +++ b/lib/infinite_scroll_pagination.dart @@ -1,8 +1,9 @@ -export 'src/core/paged_child_builder_delegate.dart'; export 'src/core/paging_controller.dart'; -export 'src/model/paging_state.dart'; -export 'src/model/paging_status.dart'; +export 'src/core/paging_state.dart'; +export 'src/core/paging_status.dart'; export 'src/base/paged_layout_builder.dart'; +export 'src/base/paged_child_builder_delegate.dart'; +export 'src/base/paging_listener.dart'; export 'src/layouts/paged_grid_view.dart'; export 'src/layouts/paged_list_view.dart'; export 'src/layouts/paged_masonry_grid_view.dart'; @@ -12,4 +13,3 @@ export 'src/layouts/paged_sliver_grid.dart'; export 'src/layouts/paged_sliver_list.dart'; export 'src/layouts/paged_sliver_masonry_grid.dart'; export 'src/layouts/paged_sliver_aligned_grid.dart'; -export 'src/base/paging_listener.dart'; diff --git a/lib/src/core/paged_child_builder_delegate.dart b/lib/src/base/paged_child_builder_delegate.dart similarity index 100% rename from lib/src/core/paged_child_builder_delegate.dart rename to lib/src/base/paged_child_builder_delegate.dart diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index 2df89b6..8e99758 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -2,15 +2,15 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_status.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_error_indicator.dart'; import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_progress_indicator.dart'; import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_error_indicator.dart'; import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_progress_indicator.dart'; import 'package:infinite_scroll_pagination/src/base/default_status_indicators/no_items_found_indicator.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_status.dart'; import 'package:sliver_tools/sliver_tools.dart'; /// Called to request a new page of data. diff --git a/lib/src/base/paging_listener.dart b/lib/src/base/paging_listener.dart index 1fa3e98..dc9c34b 100644 --- a/lib/src/base/paging_listener.dart +++ b/lib/src/base/paging_listener.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; class PagingListener diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index 9f735e7..68ad1db 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; /// A callback to get the next page key. /// If this function returns `null`, it indicates that there are no more pages to load. @@ -41,6 +41,7 @@ class PagingController /// Instead of using this property directly, use [fetchNextPage], [refresh], or [cancel]. /// If you are extending this class, check and set this property before and after the fetch operation. @protected + @visibleForTesting Object? operation; /// Fetches the next page. @@ -52,13 +53,15 @@ class PagingController final operation = this.operation = Object(); - // we use a local copy of value, - // so that we only send one notification now and at the end of the method. - PagingState state = value = value.copyWith( + value = value.copyWith( isLoading: true, error: null, ); + // we use a local copy of value, + // so that we only send one notification now and at the end of the method. + PagingState state = value; + try { // There are no more pages to load. if (!state.hasNextPage) return; diff --git a/lib/src/model/paging_state.dart b/lib/src/core/paging_state.dart similarity index 91% rename from lib/src/model/paging_state.dart rename to lib/src/core/paging_state.dart index 504366b..7889005 100644 --- a/lib/src/model/paging_state.dart +++ b/lib/src/core/paging_state.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state_base.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state_base.dart'; /// Represents the state of a paginated layout. @immutable @@ -13,6 +13,7 @@ abstract class PagingState? keys, Object? error, bool hasNextPage, + bool isLoading, }) = PagingStateBase; /// The pages fetched so far. @@ -76,6 +77,10 @@ extension ItemListExtension final class Omit implements Future { const Omit(); + // coverage:ignore-start @override - noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); + noSuchMethod(Invocation invocation) => throw UnsupportedError( + 'It is an error to attempt to use a Omit as a Future.', + ); + // coverage:ignore-end } diff --git a/lib/src/model/paging_state_base.dart b/lib/src/core/paging_state_base.dart similarity index 82% rename from lib/src/model/paging_state_base.dart rename to lib/src/core/paging_state_base.dart index 2687873..a2ce48a 100644 --- a/lib/src/model/paging_state_base.dart +++ b/lib/src/core/paging_state_base.dart @@ -1,7 +1,8 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; /// The default implementation of [PagingState]. /// @@ -80,23 +81,27 @@ base class PagingStateBase ); @override - String toString() => '${objectRuntimeType(this, 'PagingState')}' - '(pages: $pages, keys: $keys, error: $error, hasNextPage: $hasNextPage)'; + String toString() => '${objectRuntimeType(this, 'PagingStateBase')}' + '(pages: $pages, keys: $keys, error: $error, hasNextPage: $hasNextPage, ' + 'isLoading: $isLoading)'; + + static const _equality = DeepCollectionEquality(); @override bool operator ==(Object other) { return identical(this, other) || (other is PagingState && - listEquals(other.pages, pages) && - listEquals(other.keys, keys) && + _equality.equals(other.pages, pages) && + _equality.equals(other.keys, keys) && other.error == error && - other.hasNextPage == hasNextPage); + other.hasNextPage == hasNextPage && + other.isLoading == isLoading); } @override int get hashCode => Object.hash( - pages, - keys, + _equality.hash(pages), + _equality.hash(keys), error, hasNextPage, ); diff --git a/lib/src/model/paging_status.dart b/lib/src/core/paging_status.dart similarity index 91% rename from lib/src/model/paging_status.dart rename to lib/src/core/paging_status.dart index 31e093e..a85cd6f 100644 --- a/lib/src/model/paging_status.dart +++ b/lib/src/core/paging_status.dart @@ -1,4 +1,4 @@ -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; /// All possible status for a pagination. enum PagingStatus { @@ -43,6 +43,8 @@ extension PagingStatusExtension on PagingState { if (_isOngoing) return PagingStatus.ongoing; if (_hasSubsequentPageError) return PagingStatus.subsequentPageError; if (_isCompleted) return PagingStatus.completed; + // coverage:ignore-start throw StateError('Unknown status; Did you forget to implement a case?'); + // coverage:ignore-end } } diff --git a/lib/src/utils/appended_sliver_child_builder_delegate.dart b/lib/src/helpers/appended_sliver_child_builder_delegate.dart similarity index 100% rename from lib/src/utils/appended_sliver_child_builder_delegate.dart rename to lib/src/helpers/appended_sliver_child_builder_delegate.dart diff --git a/lib/src/utils/appended_sliver_grid.dart b/lib/src/helpers/appended_sliver_grid.dart similarity index 100% rename from lib/src/utils/appended_sliver_grid.dart rename to lib/src/helpers/appended_sliver_grid.dart diff --git a/lib/src/utils/listenable_listener.dart b/lib/src/helpers/listenable_listener.dart similarity index 100% rename from lib/src/utils/listenable_listener.dart rename to lib/src/helpers/listenable_listener.dart diff --git a/lib/src/layouts/paged_aligned_grid_view.dart b/lib/src/layouts/paged_aligned_grid_view.dart index 65cce19..38b1ee7 100644 --- a/lib/src/layouts/paged_aligned_grid_view.dart +++ b/lib/src/layouts/paged_aligned_grid_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_aligned_grid.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; diff --git a/lib/src/layouts/paged_grid_view.dart b/lib/src/layouts/paged_grid_view.dart index afb5cb2..5318c8b 100644 --- a/lib/src/layouts/paged_grid_view.dart +++ b/lib/src/layouts/paged_grid_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_grid.dart'; diff --git a/lib/src/layouts/paged_list_view.dart b/lib/src/layouts/paged_list_view.dart index 6ee485c..4fb9445 100644 --- a/lib/src/layouts/paged_list_view.dart +++ b/lib/src/layouts/paged_list_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_list.dart'; diff --git a/lib/src/layouts/paged_masonry_grid_view.dart b/lib/src/layouts/paged_masonry_grid_view.dart index 15579b1..b259089 100644 --- a/lib/src/layouts/paged_masonry_grid_view.dart +++ b/lib/src/layouts/paged_masonry_grid_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; diff --git a/lib/src/layouts/paged_page_view.dart b/lib/src/layouts/paged_page_view.dart index ec3226e..a30bd0f 100644 --- a/lib/src/layouts/paged_page_view.dart +++ b/lib/src/layouts/paged_page_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; /// Paged [PageView] with progress and error indicators displayed as the last diff --git a/lib/src/layouts/paged_sliver_aligned_grid.dart b/lib/src/layouts/paged_sliver_aligned_grid.dart index 0e60da6..5a322d6 100644 --- a/lib/src/layouts/paged_sliver_aligned_grid.dart +++ b/lib/src/layouts/paged_sliver_aligned_grid.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_grid.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; diff --git a/lib/src/layouts/paged_sliver_grid.dart b/lib/src/layouts/paged_sliver_grid.dart index 5e8300c..a996b62 100644 --- a/lib/src/layouts/paged_sliver_grid.dart +++ b/lib/src/layouts/paged_sliver_grid.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_grid.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_grid_view.dart'; diff --git a/lib/src/layouts/paged_sliver_list.dart b/lib/src/layouts/paged_sliver_list.dart index 815d50c..4260e96 100644 --- a/lib/src/layouts/paged_sliver_list.dart +++ b/lib/src/layouts/paged_sliver_list.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/utils/appended_sliver_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_list_view.dart'; diff --git a/lib/src/layouts/paged_sliver_masonry_grid.dart b/lib/src/layouts/paged_sliver_masonry_grid.dart index e045661..7c8a935 100644 --- a/lib/src/layouts/paged_sliver_masonry_grid.dart +++ b/lib/src/layouts/paged_sliver_masonry_grid.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:infinite_scroll_pagination/src/core/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/model/paging_state.dart'; -import 'package:infinite_scroll_pagination/src/utils/appended_sliver_grid.dart'; +import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_grid.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; typedef SliverSimpleGridDelegateBuilder = SliverSimpleGridDelegate Function( diff --git a/pubspec.yaml b/pubspec.yaml index 8624398..b16ff3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter flutter_staggered_grid_view: ^0.7.0 sliver_tools: ^0.2.12 + collection: ">=1.15.0" dev_dependencies: flutter_test: diff --git a/test/paged_layout_builder_test.dart b/test/base/paged_layout_builder_test.dart similarity index 86% rename from test/paged_layout_builder_test.dart rename to test/base/paged_layout_builder_test.dart index 93a3178..d50078a 100644 --- a/test/paged_layout_builder_test.dart +++ b/test/base/paged_layout_builder_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -7,13 +9,14 @@ import 'package:infinite_scroll_pagination/src/base/default_status_indicators/ne import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_progress_indicator.dart'; import 'package:infinite_scroll_pagination/src/base/default_status_indicators/no_items_found_indicator.dart'; -import 'utils/paging_controller_utils.dart'; +import '../utils/paging_controller_utils.dart'; void main() { group('PagingStatus.loadingFirstPage', () { - late PagingController pagingController; + late PagingState state; + setUp(() { - pagingController = PagingController(firstPageKey: 1); + state = TestPagingState.loadingFirstPage(); }); testWidgets( @@ -27,7 +30,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -50,7 +53,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -60,12 +63,10 @@ void main() { }); group('PagingStatus.firstPageError', () { - late PagingController pagingController; + late PagingState state; setUp(() { - pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnFirstPage, - ); + state = TestPagingState.firstPageError(); }); testWidgets( @@ -79,7 +80,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -102,7 +103,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -112,11 +113,10 @@ void main() { }); group('PagingStatus.noItemsFound', () { - late PagingController pagingController; + late PagingState state; + setUp(() { - pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.noItemsFound, - ); + state = TestPagingState.noItemsFound(); }); testWidgets( @@ -130,7 +130,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -153,7 +153,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -163,11 +163,10 @@ void main() { }); group('PagingStatus.subsequentPageError', () { - late PagingController pagingController; + late PagingState state; + setUp(() { - pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); + state = TestPagingState.subsequentPageError(); }); testWidgets( @@ -181,7 +180,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -207,7 +206,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -219,11 +218,10 @@ void main() { }); group('PagingStatus.ongoing', () { - late PagingController pagingController; + late PagingState state; + setUp(() { - pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); + state = TestPagingState.ongoing(); }); testWidgets( @@ -237,7 +235,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -262,7 +260,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -274,11 +272,10 @@ void main() { }); group('PagingStatus.completed', () { - late PagingController pagingController; + late PagingState state; + setUp(() { - pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.completedWithOnePage, - ); + state = TestPagingState.completed(); }); testWidgets('Uses the custom no more items indicator when one is provided.', @@ -296,7 +293,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, ); @@ -308,7 +305,7 @@ void main() { }); group('First page indicators\' height', () { - final pagingController = PagingController(firstPageKey: 1); + final state = PagingState(); const indicatorHeight = 100.0; late Key indicatorKey; late Widget progressIndicator; @@ -335,7 +332,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, shrinkWrapFirstPageIndicators: false, ); @@ -351,7 +348,7 @@ void main() { // when await _pumpPagedLayoutBuilder( tester: tester, - pagingController: pagingController, + state: state, builderDelegate: builderDelegate, shrinkWrapFirstPageIndicators: true, ); @@ -375,14 +372,15 @@ void _expectOneWidgetOfType(Type type) { Future _pumpPagedLayoutBuilder({ required WidgetTester tester, - required PagingController pagingController, + required PagingState state, required PagedChildBuilderDelegate builderDelegate, bool shrinkWrapFirstPageIndicators = false, }) => _pumpSliver( sliver: PagedLayoutBuilder( layoutProtocol: PagedLayoutProtocol.sliver, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, builderDelegate: builderDelegate, shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators, errorListingBuilder: ( diff --git a/test/core/paging_controller_test.dart b/test/core/paging_controller_test.dart new file mode 100644 index 0000000..d931fb4 --- /dev/null +++ b/test/core/paging_controller_test.dart @@ -0,0 +1,154 @@ +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +void main() { + group('PagingController', () { + late PagingController pagingController; + late int? nextPageKey; + late bool fetchCalled; + late List fetchedItems; + + setUp(() { + nextPageKey = 1; + fetchCalled = false; + fetchedItems = ['Item 1', 'Item 2']; + + getNextPageKey(state) => nextPageKey; + fetchPage(pageKey) { + fetchCalled = true; + return fetchedItems; + } + + pagingController = PagingController( + getNextPageKey: getNextPageKey, + fetchPage: fetchPage, + ); + }); + + group('fetchNextPage', () { + test('requests the next page', () async { + pagingController.fetchNextPage(); + + expect(fetchCalled, isTrue); + expect(pagingController.value.pages, [ + ['Item 1', 'Item 2'] + ]); + expect(pagingController.value.keys, [nextPageKey]); + }); + + test('fetches a page synchronously when possible', () { + pagingController.fetchNextPage(); + + expect(fetchCalled, isTrue); + expect(pagingController.value.pages, [ + ['Item 1', 'Item 2'] + ]); + expect(pagingController.value.keys, [nextPageKey]); + }); + + test('only runs one fetch at a given time', () async { + final completer = Completer>(); + + pagingController = PagingController( + getNextPageKey: (state) => nextPageKey, + fetchPage: (_) => completer.future, + ); + + pagingController.fetchNextPage(); + pagingController.fetchNextPage(); + + expect(fetchCalled, isFalse); + expect(pagingController.value.isLoading, isTrue); + + completer.complete(fetchedItems); + await Future.delayed(Duration.zero); + + expect(pagingController.value.isLoading, isFalse); + }); + + test('stops if next page key is null', () async { + nextPageKey = null; + pagingController.fetchNextPage(); + + expect(fetchCalled, isFalse); + expect(pagingController.value.hasNextPage, isFalse); + }); + + test('stops if no more pages are available', () async { + pagingController.value = + pagingController.value.copyWith(hasNextPage: false); + pagingController.fetchNextPage(); + expect(fetchCalled, isFalse); + }); + + test('catches Exceptions', () async { + pagingController = PagingController( + getNextPageKey: (state) => nextPageKey, + fetchPage: (_) => throw Exception(), + ); + + pagingController.fetchNextPage(); + + expect(pagingController.value.isLoading, isFalse); + expect(pagingController.value.error, isA()); + }); + + test('rethrows Errors', () async { + pagingController = PagingController( + getNextPageKey: (state) => nextPageKey, + fetchPage: (_) => throw Error(), + ); + + expect(() async => pagingController.fetchNextPage(), + throwsA(isA())); + expect(pagingController.value.isLoading, isFalse); + expect(pagingController.value.error, isA()); + }); + }); + + group('refresh', () { + test('resets state', () async { + pagingController.value = PagingState( + pages: const [ + ['Item 1'] + ], + keys: const [1], + ); + + pagingController.refresh(); + + expect(pagingController.value.pages, isNull); + expect(pagingController.value.keys, isNull); + expect(pagingController.value.isLoading, isFalse); + expect(pagingController.value.error, isNull); + }); + }); + + group('cancel', () { + test('resets state and stops fetch', () async { + final Completer> completer = Completer>(); + + pagingController = PagingController( + getNextPageKey: (state) => nextPageKey, + fetchPage: (_) => completer.future, + ); + + pagingController.fetchNextPage(); + + expect(pagingController.value.isLoading, isTrue); + + pagingController.cancel(); + + expect(pagingController.value.isLoading, isFalse); + completer.complete(fetchedItems); + + await Future.delayed(Duration.zero); + expect(pagingController.value.isLoading, isFalse); + expect(pagingController.value.pages, [ + ['Item 1', 'Item 2'] + ]); + }); + }); + }); +} diff --git a/test/core/paging_state_base_test.dart b/test/core/paging_state_base_test.dart new file mode 100644 index 0000000..e6221d9 --- /dev/null +++ b/test/core/paging_state_base_test.dart @@ -0,0 +1,213 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state_base.dart'; + +void main() { + group('PagingStateBase', () { + test('constructs with default values', () { + final state = PagingStateBase(); + + expect(state.pages, isNull); + expect(state.keys, isNull); + expect(state.error, isNull); + expect(state.hasNextPage, isTrue); + expect(state.isLoading, isFalse); + }); + + test('constructs with given values', () { + final state = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + expect(state.pages, [ + ['Item 1'] + ]); + expect(state.keys, [1]); + expect(state.error, 'Error message'); + expect(state.hasNextPage, isFalse); + expect(state.isLoading, isTrue); + }); + + test('creates immutable lists for pages and keys', () { + final state = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + ); + + expect(() => (state.pages as List>).add(['Item 2']), + throwsUnsupportedError); + expect(() => (state.keys as List).add(2), throwsUnsupportedError); + }); + + test('copyWith creates a copy with updated values', () { + final state = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + hasNextPage: false, + isLoading: true, + ); + + final newState = state.copyWith( + pages: [ + ['Item 2'] + ], + hasNextPage: true, + ); + + expect(newState.pages, [ + ['Item 2'] + ]); + expect(newState.keys, [1]); + expect(newState.hasNextPage, isTrue); + expect(newState.isLoading, isTrue); + }); + + test('copyWith retains values when Omit is passed', () { + final state = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Initial error', + hasNextPage: false, + isLoading: true, + ); + + final newState = state.copyWith( + pages: const Omit(), + keys: const Omit(), + error: const Omit(), + hasNextPage: const Omit(), + isLoading: const Omit(), + ); + + expect(newState.pages, state.pages); + expect(newState.keys, state.keys); + expect(newState.error, state.error); + expect(newState.hasNextPage, state.hasNextPage); + expect(newState.isLoading, state.isLoading); + }); + + test('reset creates a default state', () { + final state = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + final resetState = state.reset(); + + expect(resetState.pages, isNull); + expect(resetState.keys, isNull); + expect(resetState.error, isNull); + expect(resetState.hasNextPage, isTrue); + expect(resetState.isLoading, isFalse); + }); + + test('toString outputs expected format', () { + final state = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + expect( + state.toString(), + contains('pages: [[Item 1]]'), + ); + expect( + state.toString(), + contains('keys: [1]'), + ); + expect( + state.toString(), + contains('error: Error message'), + ); + expect( + state.toString(), + contains('hasNextPage: false'), + ); + expect( + state.toString(), + contains('isLoading: true'), + ); + }); + + test('equality works correctly', () { + final state1 = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + final state2 = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + final state3 = PagingStateBase( + pages: [ + ['Item 2'] + ], + keys: [2], + error: 'Different error', + hasNextPage: true, + isLoading: false, + ); + + expect(state1, equals(state2)); + expect(state1, isNot(equals(state3))); + }); + + test('hashCode works correctly', () { + final state1 = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + final state2 = PagingStateBase( + pages: [ + ['Item 1'] + ], + keys: [1], + error: 'Error message', + hasNextPage: false, + isLoading: true, + ); + + expect(state1.hashCode, equals(state2.hashCode)); + }); + }); +} diff --git a/test/core/paging_status_test.dart b/test/core/paging_status_test.dart new file mode 100644 index 0000000..ccfc263 --- /dev/null +++ b/test/core/paging_status_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_status.dart'; + +void main() { + group('PagingStatusExtension', () { + late PagingState pagingState; + + test( + 'returns loadingFirstPage status when loading first page with no items and no error', + () { + pagingState = PagingState(); + expect(pagingState.status, PagingStatus.loadingFirstPage); + }); + + test( + 'returns firstPageError status when first page has no items and there is an error', + () { + pagingState = PagingState(error: Exception('Error')); + expect(pagingState.status, PagingStatus.firstPageError); + }); + + test('returns noItemsFound status when there are no items and no error', + () { + pagingState = PagingState( + pages: const [], + hasNextPage: false, + ); + expect(pagingState.status, PagingStatus.noItemsFound); + }); + + test( + 'returns ongoing status when items exist, there is no error, and more pages are available', + () { + pagingState = PagingState( + pages: const [ + ['Item 1'] + ], + hasNextPage: true, + ); + expect(pagingState.status, PagingStatus.ongoing); + }); + + test( + 'returns subsequentPageError status when items exist and there is an error', + () { + pagingState = PagingState( + pages: const [ + ['Item 1'] + ], + error: Exception('Error'), + hasNextPage: true, + ); + expect(pagingState.status, PagingStatus.subsequentPageError); + }); + + test( + 'returns completed status when items exist and no more pages are available', + () { + pagingState = PagingState( + pages: const [ + ['Item 1'] + ], + hasNextPage: false, + ); + expect(pagingState.status, PagingStatus.completed); + }); + }); +} diff --git a/test/paged_grid_view_test.dart b/test/layouts/paged_grid_view_test.dart similarity index 70% rename from test/paged_grid_view_test.dart rename to test/layouts/paged_grid_view_test.dart index ffcc161..3127cd4 100644 --- a/test/paged_grid_view_test.dart +++ b/test/layouts/paged_grid_view_test.dart @@ -1,34 +1,33 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:mockito/mockito.dart'; -import 'utils/paging_controller_utils.dart'; -import 'utils/screen_size_utils.dart'; +import '../utils/paging_controller_utils.dart'; +import '../utils/screen_size_utils.dart'; double get _itemHeight => (screenSize.height / pageSize) * 2; void main() { group('Page requests', () { - late MockPageRequestListener mockPageRequestListener; + late MockFetchPageRequest mockFetchNextPage; setUp(() { - mockPageRequestListener = MockPageRequestListener(); + mockFetchNextPage = MockFetchPageRequest(); }); testWidgets('Requests first page only once', (tester) async { - final pagingController = PagingController( - firstPageKey: 1, - ); - - pagingController.addPageRequestListener(mockPageRequestListener.call); + final state = TestPagingState.loadingFirstPage(); await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: mockFetchNextPage.call, ); - verify(mockPageRequestListener(1)).called(1); + verify(mockFetchNextPage()).called(1); }); testWidgets( @@ -36,60 +35,48 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final controllerLoadedWithFirstPage = - buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - controllerLoadedWithFirstPage.addPageRequestListener( - mockPageRequestListener.call, - ); + final state = TestPagingState.ongoing(n: pageSize ~/ 2); await _pumpPagedGridView( tester: tester, - pagingController: controllerLoadedWithFirstPage, + state: state, + fetchNextPage: mockFetchNextPage.call, ); - verify(mockPageRequestListener(2)).called(1); + verify(mockFetchNextPage()).called(1); }); testWidgets('Doesn\'t request a page unnecessarily', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); + final state = TestPagingState.ongoing(n: pageSize * 2); await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, ); - verifyZeroInteractions(mockPageRequestListener); + verifyZeroInteractions(mockFetchNextPage); }); testWidgets('Requests a new page on scroll', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); + final state = TestPagingState.ongoing(n: pageSize * 2); await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: mockFetchNextPage.call, ); await tester.scrollUntilVisible( - find.text( - secondPageItemList[5], - ), + find.text('Item ${pageSize * 2}'), _itemHeight, ); - verify(mockPageRequestListener(3)).called(1); + verify(mockFetchNextPage()).called(1); }); group('Displays indicators as grid children', () { @@ -97,9 +84,7 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); + final state = TestPagingState.ongoing(); final customIndicatorKey = UniqueKey(); final customNewPageProgressIndicator = CircularProgressIndicator( @@ -108,7 +93,8 @@ void main() { await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, newPageProgressIndicator: customNewPageProgressIndicator, crossAxisCount: 2, ); @@ -128,9 +114,7 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); + final state = TestPagingState.subsequentPageError(); final customIndicatorKey = UniqueKey(); final customNewPageErrorIndicator = Text( @@ -140,7 +124,8 @@ void main() { await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, newPageErrorIndicator: customNewPageErrorIndicator, crossAxisCount: 2, ); @@ -160,9 +145,7 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.completedWithOnePage, - ); + final state = TestPagingState.completed(); final customIndicatorKey = UniqueKey(); final customNoMoreItemsIndicator = Text( @@ -172,7 +155,8 @@ void main() { await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, noMoreItemsIndicator: customNoMoreItemsIndicator, crossAxisCount: 2, ); @@ -195,9 +179,7 @@ void main() { '[showNewPageProgressIndicatorAsGridChild] is false', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); + final state = TestPagingState.ongoing(); final customIndicatorKey = UniqueKey(); final customNewPageProgressIndicator = CircularProgressIndicator( @@ -206,7 +188,8 @@ void main() { await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, newPageProgressIndicator: customNewPageProgressIndicator, showNewPageProgressIndicatorAsGridChild: false, ); @@ -222,9 +205,7 @@ void main() { '[showNewPageErrorIndicatorAsGridChild] is false', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); + final state = TestPagingState.subsequentPageError(); final customIndicatorKey = UniqueKey(); final customNewPageErrorIndicator = Text( @@ -234,7 +215,8 @@ void main() { await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, newPageErrorIndicator: customNewPageErrorIndicator, showNewPageErrorIndicatorAsGridChild: false, ); @@ -250,9 +232,7 @@ void main() { '[showNoMoreItemsIndicatorAsGridChild] is false', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.completedWithOnePage, - ); + final state = TestPagingState.completed(); final customIndicatorKey = UniqueKey(); final customNoMoreItemsIndicator = Text( @@ -262,7 +242,8 @@ void main() { await _pumpPagedGridView( tester: tester, - pagingController: pagingController, + state: state, + fetchNextPage: () => Completer().future, noMoreItemsIndicator: customNoMoreItemsIndicator, showNoMoreItemsIndicatorAsGridChild: false, ); @@ -276,13 +257,14 @@ void main() { }); } -class MockPageRequestListener extends Mock { - void call(int pageKey); +class MockFetchPageRequest extends Mock { + void call(); } Future _pumpPagedGridView({ required WidgetTester tester, - required PagingController pagingController, + required PagingState state, + required NextPageCallback fetchNextPage, int crossAxisCount = 2, Widget? newPageProgressIndicator, Widget? newPageErrorIndicator, @@ -295,9 +277,10 @@ Future _pumpPagedGridView({ MaterialApp( home: Scaffold( body: PagedGridView( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator : null, @@ -323,15 +306,3 @@ Future _pumpPagedGridView({ ), ), ); - -Widget _buildItem( - BuildContext context, - String item, - int index, -) => - SizedBox( - height: _itemHeight, - child: Text( - item, - ), - ); diff --git a/test/paged_list_view_test.dart b/test/layouts/paged_list_view_test.dart similarity index 67% rename from test/paged_list_view_test.dart rename to test/layouts/paged_list_view_test.dart index 67079a1..37ca3b3 100644 --- a/test/paged_list_view_test.dart +++ b/test/layouts/paged_list_view_test.dart @@ -3,12 +3,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:mockito/mockito.dart'; -import 'utils/paging_controller_utils.dart'; -import 'utils/screen_size_utils.dart'; +import '../utils/paging_controller_utils.dart'; +import '../utils/screen_size_utils.dart'; -const _screenSize = Size(200, 500); - -double get _itemHeight => _screenSize.height / pageSize; +double get _itemHeight => screenSize.height / pageSize; void main() { group('Page requests', () { @@ -19,51 +17,34 @@ void main() { }); testWidgets('Requests first page only once', (tester) async { - final pagingController = PagingController( - firstPageKey: 1, - ); - - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedListView( tester: tester, - pagingController: pagingController, + state: TestPagingState.loadingFirstPage(), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(1)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets( 'Requests second page immediately if the first page isn\'t enough', (tester) async { - final controllerLoadedWithFirstPage = - buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - controllerLoadedWithFirstPage.addPageRequestListener( - mockPageRequestListener.call, - ); - await _pumpPagedListView( tester: tester, - pagingController: controllerLoadedWithFirstPage, + state: TestPagingState.ongoing(n: pageSize ~/ 2), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(2)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets('Doesn\'t request a page unnecessarily', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedListView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); verifyZeroInteractions(mockPageRequestListener); @@ -72,39 +53,30 @@ void main() { testWidgets('Requests a new page on scroll', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedListView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); await tester.scrollUntilVisible( - find.text( - secondPageItemList[5], - ), + find.text('Item ${pageSize * 2}'), _itemHeight, ); - verify(mockPageRequestListener(3)).called(1); + verify(mockPageRequestListener()).called(1); }); }); testWidgets( 'Inserts separators between items if a [separatorBuilder] is specified', (tester) async { - final controllerLoadedWithFirstPage = - buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); tester.applyPreferredTestScreenSize(); await _pumpPagedListView( tester: tester, - pagingController: controllerLoadedWithFirstPage, + state: TestPagingState.ongoing(), + fetchNextPage: () {}, separatorBuilder: (_, __) => const Divider( height: 1, ), @@ -119,10 +91,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageProgressIndicator = CircularProgressIndicator( key: customIndicatorKey, @@ -130,7 +98,8 @@ void main() { await _pumpPagedListView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(), + fetchNextPage: () {}, newPageProgressIndicator: customNewPageProgressIndicator, ); @@ -149,10 +118,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageErrorIndicator = Text( 'Error', @@ -161,7 +126,8 @@ void main() { await _pumpPagedListView( tester: tester, - pagingController: pagingController, + state: TestPagingState.subsequentPageError(), + fetchNextPage: () {}, newPageErrorIndicator: customNewPageErrorIndicator, ); @@ -180,10 +146,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.completedWithOnePage, - ); - final customIndicatorKey = UniqueKey(); final customNoMoreItemsIndicator = Text( 'No More Items', @@ -192,7 +154,8 @@ void main() { await _pumpPagedListView( tester: tester, - pagingController: pagingController, + state: TestPagingState.completed(), + fetchNextPage: () {}, noMoreItemsIndicator: customNoMoreItemsIndicator, ); @@ -209,13 +172,10 @@ void main() { }); } -class MockPageRequestListener extends Mock { - void call(int pageKey); -} - Future _pumpPagedListView({ required WidgetTester tester, - required PagingController pagingController, + required PagingState state, + required NextPageCallback fetchNextPage, IndexedWidgetBuilder? separatorBuilder, Widget? newPageProgressIndicator, Widget? newPageErrorIndicator, @@ -226,9 +186,10 @@ Future _pumpPagedListView({ home: Scaffold( body: separatorBuilder == null ? PagedListView( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator @@ -242,9 +203,10 @@ Future _pumpPagedListView({ ), ) : PagedListView.separated( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator @@ -261,15 +223,3 @@ Future _pumpPagedListView({ ), ), ); - -Widget _buildItem( - BuildContext context, - String item, - int index, -) => - SizedBox( - height: _itemHeight, - child: Text( - item, - ), - ); diff --git a/test/paged_masonry_grid_view_test.dart b/test/layouts/paged_masonry_grid_view_test.dart similarity index 66% rename from test/paged_masonry_grid_view_test.dart rename to test/layouts/paged_masonry_grid_view_test.dart index 99d9792..97ec7c4 100644 --- a/test/paged_masonry_grid_view_test.dart +++ b/test/layouts/paged_masonry_grid_view_test.dart @@ -3,8 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:mockito/mockito.dart'; -import 'utils/paging_controller_utils.dart'; -import 'utils/screen_size_utils.dart'; +import '../utils/paging_controller_utils.dart'; +import '../utils/screen_size_utils.dart'; double get _itemHeight => (screenSize.height / pageSize) * 2; @@ -17,18 +17,13 @@ void main() { }); testWidgets('Requests first page only once', (tester) async { - final pagingController = PagingController( - firstPageKey: 1, - ); - - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedStaggeredGridView( tester: tester, - pagingController: pagingController, + state: TestPagingState.loadingFirstPage(), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(1)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets( @@ -36,34 +31,22 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final controllerLoadedWithFirstPage = - buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - controllerLoadedWithFirstPage.addPageRequestListener( - mockPageRequestListener.call, - ); - await _pumpPagedStaggeredGridView( tester: tester, - pagingController: controllerLoadedWithFirstPage, + state: TestPagingState.ongoing(n: pageSize ~/ 2), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(2)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets('Doesn\'t request a page unnecessarily', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedStaggeredGridView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); verifyZeroInteractions(mockPageRequestListener); @@ -72,24 +55,18 @@ void main() { testWidgets('Requests a new page on scroll', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedStaggeredGridView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); await tester.scrollUntilVisible( - find.text( - secondPageItemList[5], - ), + find.text('Item ${pageSize * 2}'), _itemHeight, ); - verify(mockPageRequestListener(3)).called(1); + verify(mockPageRequestListener()).called(1); }); group('Displays indicators as grid children', () { @@ -97,10 +74,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageProgressIndicator = CircularProgressIndicator( key: customIndicatorKey, @@ -108,7 +81,8 @@ void main() { await _pumpPagedStaggeredGridView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(), + fetchNextPage: mockPageRequestListener.call, newPageProgressIndicator: customNewPageProgressIndicator, crossAxisCount: 2, ); @@ -128,10 +102,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageErrorIndicator = Text( 'Error', @@ -140,7 +110,8 @@ void main() { await _pumpPagedStaggeredGridView( tester: tester, - pagingController: pagingController, + state: TestPagingState.subsequentPageError(), + fetchNextPage: mockPageRequestListener.call, newPageErrorIndicator: customNewPageErrorIndicator, crossAxisCount: 2, ); @@ -160,10 +131,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.completedWithOnePage, - ); - final customIndicatorKey = UniqueKey(); final customNoMoreItemsIndicator = Text( 'No More Items', @@ -172,7 +139,8 @@ void main() { await _pumpPagedStaggeredGridView( tester: tester, - pagingController: pagingController, + state: TestPagingState.completed(), + fetchNextPage: mockPageRequestListener.call, noMoreItemsIndicator: customNoMoreItemsIndicator, crossAxisCount: 2, ); @@ -191,13 +159,10 @@ void main() { }); } -class MockPageRequestListener extends Mock { - void call(int pageKey); -} - Future _pumpPagedStaggeredGridView({ required WidgetTester tester, - required PagingController pagingController, + required PagingState state, + required NextPageCallback fetchNextPage, int crossAxisCount = 2, Widget? newPageProgressIndicator, Widget? newPageErrorIndicator, @@ -207,9 +172,10 @@ Future _pumpPagedStaggeredGridView({ MaterialApp( home: Scaffold( body: PagedMasonryGridView.count( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator : null, @@ -225,15 +191,3 @@ Future _pumpPagedStaggeredGridView({ ), ), ); - -Widget _buildItem( - BuildContext context, - String item, - int index, -) => - SizedBox( - height: _itemHeight, - child: Text( - item, - ), - ); diff --git a/test/paged_page_view_test.dart b/test/layouts/paged_page_view_test.dart similarity index 63% rename from test/paged_page_view_test.dart rename to test/layouts/paged_page_view_test.dart index a424df9..e845285 100644 --- a/test/paged_page_view_test.dart +++ b/test/layouts/paged_page_view_test.dart @@ -3,8 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:mockito/mockito.dart'; -import 'utils/paging_controller_utils.dart'; -import 'utils/screen_size_utils.dart'; +import '../utils/paging_controller_utils.dart'; +import '../utils/screen_size_utils.dart'; double get _itemHeight => screenSize.height; double get _itemWidth => screenSize.width; @@ -18,31 +18,22 @@ void main() { }); testWidgets('Requests first page only once', (tester) async { - final pagingController = PagingController( - firstPageKey: 1, - ); - - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedPageView( tester: tester, - pagingController: pagingController, + state: TestPagingState.loadingFirstPage(), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(1)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets('Doesn\'t request a page unnecessarily', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedPageView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); verifyZeroInteractions(mockPageRequestListener); @@ -51,33 +42,23 @@ void main() { testWidgets('Requests a new page on scroll', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedPageView( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); await tester.scrollUntilVisible( - find.text( - firstPageItemList[8], - ), + find.text('Item ${pageSize * 2}'), 250, ); - verify(mockPageRequestListener(2)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets('Show the new page error indicator', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageErrorIndicator = Text( 'Error', @@ -86,7 +67,8 @@ void main() { await _pumpPagedPageView( tester: tester, - pagingController: pagingController, + state: TestPagingState.subsequentPageError(), + fetchNextPage: mockPageRequestListener.call, newPageErrorIndicator: customNewPageErrorIndicator, ); @@ -100,13 +82,10 @@ void main() { }); } -class MockPageRequestListener extends Mock { - void call(int pageKey); -} - Future _pumpPagedPageView({ required WidgetTester tester, - required PagingController pagingController, + required PagingState state, + required NextPageCallback fetchNextPage, Widget? newPageProgressIndicator, Widget? newPageErrorIndicator, Widget? noMoreItemsIndicator, @@ -115,10 +94,11 @@ Future _pumpPagedPageView({ MaterialApp( home: Scaffold( body: PagedPageView( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, scrollDirection: Axis.vertical, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator : null, @@ -133,15 +113,3 @@ Future _pumpPagedPageView({ ), ), ); - -Widget _buildItem( - BuildContext context, - String item, - int index, -) => - SizedBox( - height: _itemHeight, - child: Text( - item, - ), - ); diff --git a/test/paged_sliver_list_test.dart b/test/layouts/paged_sliver_list_test.dart similarity index 67% rename from test/paged_sliver_list_test.dart rename to test/layouts/paged_sliver_list_test.dart index 70f522e..61939ac 100644 --- a/test/paged_sliver_list_test.dart +++ b/test/layouts/paged_sliver_list_test.dart @@ -3,12 +3,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:mockito/mockito.dart'; -import 'utils/paging_controller_utils.dart'; -import 'utils/screen_size_utils.dart'; +import '../utils/paging_controller_utils.dart'; +import '../utils/screen_size_utils.dart'; -const _screenSize = Size(200, 500); - -double get _itemHeight => _screenSize.height / pageSize; +double get _itemHeight => screenSize.height / pageSize; void main() { group('Page requests', () { @@ -19,51 +17,34 @@ void main() { }); testWidgets('Requests first page only once', (tester) async { - final pagingController = PagingController( - firstPageKey: 1, - ); - - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedSliverList( tester: tester, - pagingController: pagingController, + state: TestPagingState.loadingFirstPage(), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(1)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets( 'Requests second page immediately if the first page isn\'t enough', (tester) async { - final controllerLoadedWithFirstPage = - buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - controllerLoadedWithFirstPage.addPageRequestListener( - mockPageRequestListener.call, - ); - await _pumpPagedSliverList( tester: tester, - pagingController: controllerLoadedWithFirstPage, + state: TestPagingState.ongoing(n: pageSize ~/ 2), + fetchNextPage: mockPageRequestListener.call, ); - verify(mockPageRequestListener(2)).called(1); + verify(mockPageRequestListener()).called(1); }); testWidgets('Doesn\'t request a page unnecessarily', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedSliverList( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); verifyZeroInteractions(mockPageRequestListener); @@ -72,39 +53,30 @@ void main() { testWidgets('Requests a new page on scroll', (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithTwoPages, - ); - pagingController.addPageRequestListener(mockPageRequestListener.call); - await _pumpPagedSliverList( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(n: pageSize * 2), + fetchNextPage: mockPageRequestListener.call, ); await tester.scrollUntilVisible( - find.text( - secondPageItemList[5], - ), + find.text('Item ${pageSize * 2}'), _itemHeight, ); - verify(mockPageRequestListener(3)).called(1); + verify(mockPageRequestListener()).called(1); }); }); testWidgets( 'Inserts separators between items if a [separatorBuilder] is specified', (tester) async { - final controllerLoadedWithFirstPage = - buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); tester.applyPreferredTestScreenSize(); await _pumpPagedSliverList( tester: tester, - pagingController: controllerLoadedWithFirstPage, + state: TestPagingState.ongoing(), + fetchNextPage: () {}, separatorBuilder: (_, __) => const Divider( height: 1, ), @@ -119,10 +91,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageProgressIndicator = CircularProgressIndicator( key: customIndicatorKey, @@ -130,7 +98,8 @@ void main() { await _pumpPagedSliverList( tester: tester, - pagingController: pagingController, + state: TestPagingState.ongoing(), + fetchNextPage: () {}, newPageProgressIndicator: customNewPageProgressIndicator, ); @@ -149,10 +118,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - final customIndicatorKey = UniqueKey(); final customNewPageErrorIndicator = Text( 'Error', @@ -161,7 +126,8 @@ void main() { await _pumpPagedSliverList( tester: tester, - pagingController: pagingController, + state: TestPagingState.subsequentPageError(), + fetchNextPage: () {}, newPageErrorIndicator: customNewPageErrorIndicator, ); @@ -180,10 +146,6 @@ void main() { (tester) async { tester.applyPreferredTestScreenSize(); - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.completedWithOnePage, - ); - final customIndicatorKey = UniqueKey(); final customNoMoreItemsIndicator = Text( 'No More Items', @@ -192,7 +154,8 @@ void main() { await _pumpPagedSliverList( tester: tester, - pagingController: pagingController, + state: TestPagingState.completed(), + fetchNextPage: () {}, noMoreItemsIndicator: customNoMoreItemsIndicator, ); @@ -209,13 +172,10 @@ void main() { }); } -class MockPageRequestListener extends Mock { - void call(int pageKey); -} - Future _pumpPagedSliverList({ required WidgetTester tester, - required PagingController pagingController, + required PagingState state, + required NextPageCallback fetchNextPage, IndexedWidgetBuilder? separatorBuilder, Widget? newPageProgressIndicator, Widget? newPageErrorIndicator, @@ -228,9 +188,10 @@ Future _pumpPagedSliverList({ slivers: [ if (separatorBuilder == null) PagedSliverList( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator @@ -245,9 +206,10 @@ Future _pumpPagedSliverList({ ) else PagedSliverList.separated( - pagingController: pagingController, + state: state, + fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: _buildItem, + itemBuilder: buildTestTile(_itemHeight), newPageProgressIndicatorBuilder: newPageProgressIndicator != null ? (context) => newPageProgressIndicator @@ -266,15 +228,3 @@ Future _pumpPagedSliverList({ ), ), ); - -Widget _buildItem( - BuildContext context, - String item, - int index, -) => - SizedBox( - height: _itemHeight, - child: Text( - item, - ), - ); diff --git a/test/paging_controller_test.dart b/test/paging_controller_test.dart deleted file mode 100644 index 59f6a21..0000000 --- a/test/paging_controller_test.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:mockito/mockito.dart'; - -import 'utils/paging_controller_utils.dart'; - -void main() { - group('[appendPage]', () { - test('Appends the new list to [itemList]', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // when - pagingController.appendPage(secondPageItemList, 2); - - // then - expect(pagingController.itemList, [ - ...firstPageItemList, - ...secondPageItemList, - ]); - }); - - test('Changes [nextPageKey]', () { - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - const newNextPageKey = 3; - - // when - pagingController.appendPage(secondPageItemList, newNextPageKey); - - // then - expect(pagingController.nextPageKey, newNextPageKey); - }); - - test('Sets [error] to null', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - - // when - pagingController.appendPage(secondPageItemList, 3); - - // then - expect(pagingController.error, null); - }); - }); - - group('[appendLastPage]', () { - test('Appends the new list to [itemList]', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // when - pagingController.appendLastPage(secondPageItemList); - - // then - expect(pagingController.itemList, [ - ...firstPageItemList, - ...secondPageItemList, - ]); - }); - - test('Sets [nextPageKey] to null', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // when - pagingController.appendLastPage(secondPageItemList); - - // then - expect(pagingController.nextPageKey, null); - }); - - test('Sets [error] to null', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - - // when - pagingController.appendLastPage(secondPageItemList); - - // then - expect(pagingController.error, null); - }); - }); - - test('[retryLastFailedRequest] sets [error] to null', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - - // when - pagingController.retryLastFailedRequest(); - - // then - expect(pagingController.error, null); - }); - - group('[refresh]', () { - test('Sets [itemList] to null', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // when - pagingController.refresh(); - - // then - expect(pagingController.itemList, null); - }); - - test('Sets [error] to null', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.errorOnSecondPage, - ); - - // when - pagingController.refresh(); - - // then - expect(pagingController.error, null); - }); - - test('Sets [nextPageKey] back to [firstPageKey]', () { - // given - final pagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // when - pagingController.refresh(); - - // then - expect(pagingController.nextPageKey, 1); - }); - }); - - group('[PagingStatusListener]', () { - late PagingController pagingController; - late PagingStatusListener mockStatusListener; - - setUp(() { - pagingController = PagingController(firstPageKey: 1); - mockStatusListener = MockStatusListener().call; - pagingController.addStatusListener(mockStatusListener); - }); - - test('Assigning a new [value] notifies [PagingStatusListener]', () { - // when - pagingController.value = buildPagingStateWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // then - verify(mockStatusListener(PagingStatus.ongoing)); - }); - - test('Removed [PagingStatusListener]s aren\'t notified', () { - // when - pagingController.removeStatusListener(mockStatusListener); - pagingController.value = buildPagingStateWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - ); - - // then - verifyNever(mockStatusListener(PagingStatus.ongoing)); - }); - }); - - group('[PageRequestListener]', () { - late PagingController pagingController; - late PageRequestListener mockPageRequestListener; - const requestedPageKey = 2; - - setUp(() { - pagingController = PagingController(firstPageKey: 1); - mockPageRequestListener = MockPageRequestListener().call; - pagingController.addPageRequestListener(mockPageRequestListener); - }); - - test('[PageRequestListener]s are notified', () { - // when - pagingController.notifyPageRequestListeners(requestedPageKey); - - // then - verify(mockPageRequestListener(requestedPageKey)); - }); - - test('Removed [PageRequestListener]s aren\'t notified', () { - // when - pagingController.removePageRequestListener(mockPageRequestListener); - pagingController.notifyPageRequestListeners(requestedPageKey); - - // then - verifyNever(mockPageRequestListener(requestedPageKey)); - }); - }); - - group('[dispose]', () { - late PagingController disposedPagingController; - setUp(() { - disposedPagingController = buildPagingControllerWithPopulatedState( - PopulatedStateOption.ongoingWithOnePage, - )..dispose(); - }); - - test('Can\'t add a [PageRequestListener] to a disposed [PagingController]', - () { - expect( - () => disposedPagingController.addPageRequestListener((pageKey) {}), - throwsException, - ); - }); - - test('Can\'t add a [PagingStatusListener] to a disposed PagingController', - () { - expect( - () => disposedPagingController.addStatusListener((status) {}), - throwsException, - ); - }); - - test( - 'Can\'t remove a [PageRequestListener] from a disposed ' - '[PagingController]', () { - expect( - () => disposedPagingController.removePageRequestListener((pageKey) {}), - throwsException, - ); - }); - - test( - 'Can\'t remove a [PagingStatusListener] from a disposed ' - '[PagingController]', () { - expect( - () => disposedPagingController.removeStatusListener((status) {}), - throwsException, - ); - }); - - test( - 'Can\'t notify [PageRequestListener]s from a disposed ' - '[PagingController]', () { - expect( - () => disposedPagingController.notifyPageRequestListeners(3), - throwsException, - ); - }); - - test( - 'Can\'t notify [PagingStatusListener]s from a disposed ' - '[PagingController]', () { - expect( - () => disposedPagingController.notifyStatusListeners( - PagingStatus.subsequentPageError, - ), - throwsException, - ); - }); - }); - - group('Computed Properties tests', () { - late PagingController pagingController; - - setUp(() { - pagingController = PagingController(firstPageKey: 1); - }); - - test('Assigning to [itemList] changes [value]', () { - // when - pagingController.itemList = firstPageItemList; - - // then - expect(pagingController.value.itemList, firstPageItemList); - }); - - test('Assigning to [nextPageKey] changes [value]', () { - // when - const nextPageKey = 2; - pagingController.nextPageKey = nextPageKey; - - // then - expect(pagingController.value.nextPageKey, nextPageKey); - }); - - test('Assigning to [error] changes [value]', () { - // when - final error = Error(); - pagingController.error = error; - - // then - expect(pagingController.value.error, error); - }); - }); -} - -class MockStatusListener extends Mock { - void call(PagingStatus status); -} - -class MockPageRequestListener extends Mock { - void call(PageKeyType pageKey); -} diff --git a/test/paging_state_test.dart b/test/paging_state_test.dart deleted file mode 100644 index 88fc00e..0000000 --- a/test/paging_state_test.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; - -void main() { - group('[status]', () { - test( - 'When [itemList] isn\'t empty, [nextPageKey] isn\'t null, ' - 'and [error] is null, [status] should be [PagingStatus.ongoing]', () { - const pagingState = PagingState( - nextPageKey: 2, - error: null, - itemList: [1, 2], - ); - - expect(pagingState.status, PagingStatus.ongoing); - }); - - test( - 'When [itemList] isn\'t empty, and [nextPageKey] is null, ' - '[status] should be [PagingStatus.completed]', () { - const pagingState = PagingState( - nextPageKey: null, - error: null, - itemList: [1, 2], - ); - - expect(pagingState.status, PagingStatus.completed); - }); - - test( - 'When both [itemList] and [error] are null, ' - '[status] should be [PagingStatus.loadingFirstPage]', () { - const pagingState = PagingState( - nextPageKey: null, - error: null, - itemList: null, - ); - - expect(pagingState.status, PagingStatus.loadingFirstPage); - }); - - test( - 'When [itemList] isn\'t empty, [nextPageKey] isn\'t null, and ' - '[error] isn\'t null, ' - '[status] should be [PagingStatus.subsequentPageError]', () { - final pagingState = PagingState( - nextPageKey: 1, - error: Error(), - itemList: const [1, 2], - ); - - expect(pagingState.status, PagingStatus.subsequentPageError); - }); - - test( - 'When [itemList] is empty, ' - '[status] should be [PagingStatus.noItemsFound]', () { - const pagingState = PagingState( - nextPageKey: null, - error: null, - itemList: [], - ); - - expect(pagingState.status, PagingStatus.noItemsFound); - }); - }); - - test('Two different instances with equal properties are considered equal', - () { - const pagingState1 = PagingState( - nextPageKey: 2, - itemList: [1, 2], - error: null, - ); - const pagingState2 = PagingState( - nextPageKey: 2, - itemList: [1, 2], - error: null, - ); - expect(pagingState1, pagingState2); - }); - - test('[toString] returns the correct values', () { - const pagingState = PagingState( - nextPageKey: 2, - error: null, - itemList: [1], - ); - - expect( - pagingState.toString(), - 'PagingState(itemList: ┤[1]├, error: null, nextPageKey: 2)', - ); - }); - - group('[hashCode]', () { - test('Equal [PagingState]s have equal [hashCode]s', () { - const pagingState1 = PagingState( - nextPageKey: 2, - itemList: [1, 2], - error: null, - ); - const pagingState2 = PagingState( - nextPageKey: 2, - itemList: [1, 2], - error: null, - ); - - expect(pagingState1.hashCode, pagingState2.hashCode); - }); - - test('Different [PagingState]s have different [hashCode]s', () { - const pagingState1 = PagingState( - nextPageKey: 2, - itemList: [1, 2], - error: null, - ); - - const pagingState2 = PagingState( - nextPageKey: 3, - itemList: [1, 2, 3, 4], - error: null, - ); - - expect(pagingState1.hashCode, isNot(pagingState2.hashCode)); - }); - }); -} diff --git a/test/utils/paging_controller_utils.dart b/test/utils/paging_controller_utils.dart index a13c622..8f9852d 100644 --- a/test/utils/paging_controller_utils.dart +++ b/test/utils/paging_controller_utils.dart @@ -1,73 +1,65 @@ +import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:mockito/mockito.dart'; -const firstPageItemList = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; +class MockPageRequestListener extends Mock { + void call(); +} -const secondPageItemList = [ - '11', - '12', - '13', - '14', - '15', - '16', - '17', - '18', - '19', - '20' -]; +class TestException implements Exception {} -const pageSize = 10; +const int pageSize = 10; -PagingState buildPagingStateWithPopulatedState( - PopulatedStateOption filledStateOption, -) { - switch (filledStateOption) { - case PopulatedStateOption.completedWithOnePage: - return const PagingState( - nextPageKey: null, - itemList: firstPageItemList, - ); - case PopulatedStateOption.errorOnSecondPage: - return PagingState( - nextPageKey: 2, - itemList: firstPageItemList, - error: Error(), - ); - case PopulatedStateOption.ongoingWithOnePage: - return const PagingState( - nextPageKey: 2, - itemList: firstPageItemList, - ); - case PopulatedStateOption.ongoingWithTwoPages: - return const PagingState( - nextPageKey: 3, - itemList: [...firstPageItemList, ...secondPageItemList], - ); - case PopulatedStateOption.errorOnFirstPage: - return PagingState( - error: Error(), +List generateItems(int count) => + List.generate(count, (index) => 'Item ${index + 1}'); + +extension TestPagingState on PagingState { + static PagingState loadingFirstPage() => + PagingState(); + + static PagingState firstPageError() => + PagingState(error: TestException()); + + static PagingState noItemsFound() => PagingState( + pages: const [], + keys: const [1], + hasNextPage: false, ); - case PopulatedStateOption.noItemsFound: - return const PagingState( - itemList: [], + + static PagingState ongoing({int n = pageSize}) => + PagingState( + pages: [generateItems(n)], + keys: const [1], + hasNextPage: true, ); - } -} -PagingController buildPagingControllerWithPopulatedState( - PopulatedStateOption filledStateOption, -) { - final state = buildPagingStateWithPopulatedState( - filledStateOption, - ); + static PagingState subsequentPageError({int n = pageSize}) => + PagingState( + pages: [generateItems(n)], + keys: const [1], + error: TestException(), + hasNextPage: true, + ); - return PagingController.fromValue(state, firstPageKey: 1); + static PagingState completed({int n = pageSize}) => + PagingState( + pages: [generateItems(n)], + keys: const [1], + hasNextPage: false, + ); } -enum PopulatedStateOption { - errorOnSecondPage, - completedWithOnePage, - ongoingWithTwoPages, - ongoingWithOnePage, - errorOnFirstPage, - noItemsFound, +Widget Function(BuildContext, String, int) buildTestTile(double itemHeight) { + return ( + BuildContext context, + String item, + int index, + ) { + return SizedBox( + height: itemHeight, + child: Text( + item, + ), + ); + }; } From 07dfe6de0965eaeb1597e662204d9e874a0fc2b1 Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 13 Dec 2024 03:15:52 +0100 Subject: [PATCH 12/37] chore: remove listenable listener --- lib/src/helpers/listenable_listener.dart | 58 ------------------------ 1 file changed, 58 deletions(-) delete mode 100644 lib/src/helpers/listenable_listener.dart diff --git a/lib/src/helpers/listenable_listener.dart b/lib/src/helpers/listenable_listener.dart deleted file mode 100644 index 2ff6224..0000000 --- a/lib/src/helpers/listenable_listener.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// A widget that calls [listener] when the given [Listenable] changes value. -class ListenableListener extends StatefulWidget { - const ListenableListener({ - required this.listenable, - required this.child, - this.listener, - super.key, - }); - - /// The [Listenable] to which this widget is listening. - /// - /// Commonly an [Animation] or a [ChangeNotifier]. - final Listenable listenable; - - /// Called every time the [listenable] changes value. - final VoidCallback? listener; - - /// The widget below this widget in the tree. - final Widget child; - - @override - State createState() => _ListenableListenerState(); -} - -class _ListenableListenerState extends State { - Listenable get _listenable => widget.listenable; - - @override - void initState() { - super.initState(); - _listenable.addListener(_handleChange); - _handleChange(); - } - - @override - void didUpdateWidget(ListenableListener oldWidget) { - super.didUpdateWidget(oldWidget); - if (_listenable != oldWidget.listenable) { - oldWidget.listenable.removeListener(_handleChange); - _listenable.addListener(_handleChange); - } - } - - @override - void dispose() { - _listenable.removeListener(_handleChange); - super.dispose(); - } - - void _handleChange() { - widget.listener?.call(); - } - - @override - Widget build(BuildContext context) => widget.child; -} From 4d76b8bde8d218a7a5d89a7d666e9be9e5be1982 Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 13 Dec 2024 03:22:29 +0100 Subject: [PATCH 13/37] chore: move default widgets --- lib/src/base/paged_layout_builder.dart | 10 +++++----- .../first_page_error_indicator.dart | 2 +- .../first_page_exception_indicator.dart | 0 .../first_page_progress_indicator.dart | 0 .../footer_tile.dart | 0 .../new_page_error_indicator.dart | 2 +- .../new_page_progress_indicator.dart | 2 +- .../no_items_found_indicator.dart | 2 +- test/base/paged_layout_builder_test.dart | 10 +++++----- 9 files changed, 14 insertions(+), 14 deletions(-) rename lib/src/{base/default_status_indicators => defaults}/first_page_error_indicator.dart (80%) rename lib/src/{base/default_status_indicators => defaults}/first_page_exception_indicator.dart (100%) rename lib/src/{base/default_status_indicators => defaults}/first_page_progress_indicator.dart (100%) rename lib/src/{base/default_status_indicators => defaults}/footer_tile.dart (100%) rename lib/src/{base/default_status_indicators => defaults}/new_page_error_indicator.dart (89%) rename lib/src/{base/default_status_indicators => defaults}/new_page_progress_indicator.dart (73%) rename lib/src/{base/default_status_indicators => defaults}/no_items_found_indicator.dart (75%) diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index 8e99758..d4e082a 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/no_items_found_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/first_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/first_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/new_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/new_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/no_items_found_indicator.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/core/paging_status.dart'; import 'package:sliver_tools/sliver_tools.dart'; diff --git a/lib/src/base/default_status_indicators/first_page_error_indicator.dart b/lib/src/defaults/first_page_error_indicator.dart similarity index 80% rename from lib/src/base/default_status_indicators/first_page_error_indicator.dart rename to lib/src/defaults/first_page_error_indicator.dart index fd83e1e..7fb879a 100644 --- a/lib/src/base/default_status_indicators/first_page_error_indicator.dart +++ b/lib/src/defaults/first_page_error_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_exception_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/first_page_exception_indicator.dart'; class FirstPageErrorIndicator extends StatelessWidget { const FirstPageErrorIndicator({ diff --git a/lib/src/base/default_status_indicators/first_page_exception_indicator.dart b/lib/src/defaults/first_page_exception_indicator.dart similarity index 100% rename from lib/src/base/default_status_indicators/first_page_exception_indicator.dart rename to lib/src/defaults/first_page_exception_indicator.dart diff --git a/lib/src/base/default_status_indicators/first_page_progress_indicator.dart b/lib/src/defaults/first_page_progress_indicator.dart similarity index 100% rename from lib/src/base/default_status_indicators/first_page_progress_indicator.dart rename to lib/src/defaults/first_page_progress_indicator.dart diff --git a/lib/src/base/default_status_indicators/footer_tile.dart b/lib/src/defaults/footer_tile.dart similarity index 100% rename from lib/src/base/default_status_indicators/footer_tile.dart rename to lib/src/defaults/footer_tile.dart diff --git a/lib/src/base/default_status_indicators/new_page_error_indicator.dart b/lib/src/defaults/new_page_error_indicator.dart similarity index 89% rename from lib/src/base/default_status_indicators/new_page_error_indicator.dart rename to lib/src/defaults/new_page_error_indicator.dart index 94a2fa4..124c9e0 100644 --- a/lib/src/base/default_status_indicators/new_page_error_indicator.dart +++ b/lib/src/defaults/new_page_error_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/footer_tile.dart'; +import 'package:infinite_scroll_pagination/src/defaults/footer_tile.dart'; class NewPageErrorIndicator extends StatelessWidget { const NewPageErrorIndicator({ diff --git a/lib/src/base/default_status_indicators/new_page_progress_indicator.dart b/lib/src/defaults/new_page_progress_indicator.dart similarity index 73% rename from lib/src/base/default_status_indicators/new_page_progress_indicator.dart rename to lib/src/defaults/new_page_progress_indicator.dart index 870d1bb..98fbe0d 100644 --- a/lib/src/base/default_status_indicators/new_page_progress_indicator.dart +++ b/lib/src/defaults/new_page_progress_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/footer_tile.dart'; +import 'package:infinite_scroll_pagination/src/defaults/footer_tile.dart'; class NewPageProgressIndicator extends StatelessWidget { const NewPageProgressIndicator({super.key}); diff --git a/lib/src/base/default_status_indicators/no_items_found_indicator.dart b/lib/src/defaults/no_items_found_indicator.dart similarity index 75% rename from lib/src/base/default_status_indicators/no_items_found_indicator.dart rename to lib/src/defaults/no_items_found_indicator.dart index 9d9c1ff..7dd52f8 100644 --- a/lib/src/base/default_status_indicators/no_items_found_indicator.dart +++ b/lib/src/defaults/no_items_found_indicator.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_exception_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/first_page_exception_indicator.dart'; class NoItemsFoundIndicator extends StatelessWidget { const NoItemsFoundIndicator({super.key}); diff --git a/test/base/paged_layout_builder_test.dart b/test/base/paged_layout_builder_test.dart index d50078a..3114371 100644 --- a/test/base/paged_layout_builder_test.dart +++ b/test/base/paged_layout_builder_test.dart @@ -3,11 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/first_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_error_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/new_page_progress_indicator.dart'; -import 'package:infinite_scroll_pagination/src/base/default_status_indicators/no_items_found_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/first_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/first_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/new_page_error_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/new_page_progress_indicator.dart'; +import 'package:infinite_scroll_pagination/src/defaults/no_items_found_indicator.dart'; import '../utils/paging_controller_utils.dart'; From a9ca183b18872277fcc82d0eb9e1bff7d3b3aaaa Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 13 Dec 2024 03:36:51 +0100 Subject: [PATCH 14/37] chore: update exports --- lib/infinite_scroll_pagination.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/infinite_scroll_pagination.dart b/lib/infinite_scroll_pagination.dart index e3baa22..aa452e6 100644 --- a/lib/infinite_scroll_pagination.dart +++ b/lib/infinite_scroll_pagination.dart @@ -1,15 +1,21 @@ +export 'src/base/paged_child_builder_delegate.dart'; +export 'src/base/paged_layout_builder.dart'; +export 'src/base/paging_listener.dart'; + export 'src/core/paging_controller.dart'; export 'src/core/paging_state.dart'; +export 'src/core/paging_state_base.dart'; export 'src/core/paging_status.dart'; -export 'src/base/paged_layout_builder.dart'; -export 'src/base/paged_child_builder_delegate.dart'; -export 'src/base/paging_listener.dart'; + +export 'src/helpers/appended_sliver_child_builder_delegate.dart'; +export 'src/helpers/appended_sliver_grid.dart'; + +export 'src/layouts/paged_aligned_grid_view.dart'; export 'src/layouts/paged_grid_view.dart'; export 'src/layouts/paged_list_view.dart'; export 'src/layouts/paged_masonry_grid_view.dart'; -export 'src/layouts/paged_aligned_grid_view.dart'; export 'src/layouts/paged_page_view.dart'; +export 'src/layouts/paged_sliver_aligned_grid.dart'; export 'src/layouts/paged_sliver_grid.dart'; export 'src/layouts/paged_sliver_list.dart'; export 'src/layouts/paged_sliver_masonry_grid.dart'; -export 'src/layouts/paged_sliver_aligned_grid.dart'; From ce2689fef363ff33a8fd52ae0fa56c2f721afbbf Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 13 Dec 2024 16:50:05 +0100 Subject: [PATCH 15/37] docs: update setstate example --- example/lib/samples/page_view.dart | 71 ++++++++++++++++++------------ lib/src/core/paging_status.dart | 6 +-- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/example/lib/samples/page_view.dart b/example/lib/samples/page_view.dart index 81e4edc..816e4ae 100644 --- a/example/lib/samples/page_view.dart +++ b/example/lib/samples/page_view.dart @@ -3,6 +3,8 @@ import 'package:infinite_example/remote/api.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'list_view.dart'; +import 'sliver_grid.dart'; class PageViewScreen extends StatefulWidget { const PageViewScreen({super.key}); @@ -14,39 +16,54 @@ class PageViewScreen extends StatefulWidget { class _PageViewScreenState extends State { final PageController _pageController = PageController(); + /// This example uses a [PagingState] and [setState] directly to manage the paging state. + /// + /// This is the most direct way to use the package. + /// For a more managed approach, see [ListViewScreen]. + /// For managing your [PagingState] inside your own controller, see [SliverGridScreen]. PagingState _state = PagingState(); void fetchNextPage() async { if (_state.isLoading) return; - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - _state = _state.copyWith(isLoading: true, error: null); - }); + // we wait one tick to avoid calling setState at the wrong time + await Future.value(); + + setState(() { + // set loading to true and remove any previous error + _state = _state.copyWith(isLoading: true, error: null); }); try { + // in our simple setup, keys are sequential numbers final newKey = (_state.keys?.last ?? 0) + 1; + // we fetch the next page of items final newItems = await RemoteApi.getPhotos(newKey); + // if the new page is empty, we reached the end final isLastPage = newItems.isEmpty; setState(() { _state = _state.copyWith( + // append our new page to the existing pages pages: [ ...?_state.pages, newItems, ], + // append the new key to the existing keys keys: [ ...?_state.keys, newKey, ], + // signal if we reached the end hasNextPage: !isLastPage, + // set loading back to false isLoading: false, ); }); } catch (error) { setState(() { _state = _state.copyWith( + // in case of an error, we store it in the state error: error, isLoading: false, ); @@ -75,31 +92,29 @@ class _PageViewScreenState extends State { bottom: 16, child: ListenableBuilder( listenable: _pageController, - builder: (context, _) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_pageController.hasClients) - Material( - borderRadius: BorderRadius.circular(4), - color: Colors.black38, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - child: Text( - '${(_pageController.page ?? 0).round()} / ${_state.items?.length ?? 0}', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(color: Colors.white), - ), + builder: (context, _) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_pageController.hasClients) + Material( + borderRadius: BorderRadius.circular(4), + color: Colors.black38, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, ), - ) - ], - ); - }, + child: Text( + '${(_pageController.page ?? 0).round()} / ${_state.items?.length ?? 0}', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: Colors.white), + ), + ), + ) + ], + ), ), ), ], diff --git a/lib/src/core/paging_status.dart b/lib/src/core/paging_status.dart index a85cd6f..bb8c23c 100644 --- a/lib/src/core/paging_status.dart +++ b/lib/src/core/paging_status.dart @@ -2,12 +2,12 @@ import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; /// All possible status for a pagination. enum PagingStatus { - completed, noItemsFound, loadingFirstPage, - ongoing, firstPageError, + ongoing, subsequentPageError, + completed, } /// Extension methods for [PagingState] to determine the current status. @@ -43,7 +43,7 @@ extension PagingStatusExtension on PagingState { if (_isOngoing) return PagingStatus.ongoing; if (_hasSubsequentPageError) return PagingStatus.subsequentPageError; if (_isCompleted) return PagingStatus.completed; - // coverage:ignore-start + // coverage:ignore-start // This can never happen under normal circumstances. throw StateError('Unknown status; Did you forget to implement a case?'); // coverage:ignore-end } From a5bebfad09d9d99d75bee9d00483f6c353a12a39 Mon Sep 17 00:00:00 2001 From: clragon Date: Thu, 9 Jan 2025 22:23:04 +0100 Subject: [PATCH 16/37] docs: update diagram --- assets/api-diagram.drawio | 193 ++++++++++++++------------------------ assets/api-diagram.png | Bin 141247 -> 84900 bytes 2 files changed, 70 insertions(+), 123 deletions(-) diff --git a/assets/api-diagram.drawio b/assets/api-diagram.drawio index d29b316..993462e 100644 --- a/assets/api-diagram.drawio +++ b/assets/api-diagram.drawio @@ -1,168 +1,115 @@ - + - + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - + + - - + + - - + + - + - + - - + + - - - - - + + + + + - - + + - - + + - - + + - + - + - - - - - - - - + + - - - - - - - - - - - + + + + + - - + + - - + + - - + + - + - + - - + + - + - - + + - - - - - + + - - - - - - - - - - + + - - + + - + - + - - - - - - - - - - - + + - - + + diff --git a/assets/api-diagram.png b/assets/api-diagram.png index 72b517c409776f407b27bad717719bee96dff1bb..298be94720868c53d4d7cb03f40937fb24d70d96 100644 GIT binary patch literal 84900 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 `!v;>#^WuqYD62`QnoN2omF3iS(f;X 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

QdRCdLPwQH^r0 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 From 05b15e7b07c38377c5e6d0f3c26836631205093a Mon Sep 17 00:00:00 2001 From: clragon Date: Thu, 9 Jan 2025 22:31:12 +0100 Subject: [PATCH 17/37] docs: move example markdown --- example/{README.md => example.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example/{README.md => example.md} (100%) diff --git a/example/README.md b/example/example.md similarity index 100% rename from example/README.md rename to example/example.md From 9af54bb4ca966e6fbff365751a63e38f66b4fdab Mon Sep 17 00:00:00 2001 From: clragon Date: Thu, 9 Jan 2025 22:34:10 +0100 Subject: [PATCH 18/37] docs: add readme for example project --- example/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 example/README.md diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..a639b86 --- /dev/null +++ b/example/README.md @@ -0,0 +1,11 @@ +# Example + +To run the example project, make sure to enable the corresponding platform, with + +```sh +flutter create ./example --platforms= +``` + +## Cookbook + +The cookbook file is can be found here: [./example.md](./example.md) From 4535f0d16ce87875926e024895cac3d8302f9a12 Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 10 Jan 2025 04:10:04 +0100 Subject: [PATCH 19/37] feat: add staggered grid view export --- lib/infinite_scroll_pagination.dart | 1 + lib/src/helpers/flutter_staggered_grid_view.dart | 7 +++++++ lib/src/layouts/paged_aligned_grid_view.dart | 3 +-- lib/src/layouts/paged_masonry_grid_view.dart | 2 +- lib/src/layouts/paged_sliver_aligned_grid.dart | 3 +-- lib/src/layouts/paged_sliver_masonry_grid.dart | 6 +----- 6 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 lib/src/helpers/flutter_staggered_grid_view.dart diff --git a/lib/infinite_scroll_pagination.dart b/lib/infinite_scroll_pagination.dart index aa452e6..2a4b8ed 100644 --- a/lib/infinite_scroll_pagination.dart +++ b/lib/infinite_scroll_pagination.dart @@ -9,6 +9,7 @@ export 'src/core/paging_status.dart'; export 'src/helpers/appended_sliver_child_builder_delegate.dart'; export 'src/helpers/appended_sliver_grid.dart'; +export 'src/helpers/flutter_staggered_grid_view.dart'; export 'src/layouts/paged_aligned_grid_view.dart'; export 'src/layouts/paged_grid_view.dart'; diff --git a/lib/src/helpers/flutter_staggered_grid_view.dart b/lib/src/helpers/flutter_staggered_grid_view.dart new file mode 100644 index 0000000..70d10b7 --- /dev/null +++ b/lib/src/helpers/flutter_staggered_grid_view.dart @@ -0,0 +1,7 @@ +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +export 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +typedef SliverSimpleGridDelegateBuilder = SliverSimpleGridDelegate Function( + int childCount, +); diff --git a/lib/src/layouts/paged_aligned_grid_view.dart b/lib/src/layouts/paged_aligned_grid_view.dart index 38b1ee7..a112c37 100644 --- a/lib/src/layouts/paged_aligned_grid_view.dart +++ b/lib/src/layouts/paged_aligned_grid_view.dart @@ -1,10 +1,9 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/helpers/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_aligned_grid.dart'; -import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; /// A [AlignedGridView] with pagination capabilities. /// diff --git a/lib/src/layouts/paged_masonry_grid_view.dart b/lib/src/layouts/paged_masonry_grid_view.dart index b259089..f584144 100644 --- a/lib/src/layouts/paged_masonry_grid_view.dart +++ b/lib/src/layouts/paged_masonry_grid_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; +import 'package:infinite_scroll_pagination/src/helpers/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; /// A [MasonryGridView] with pagination capabilities. diff --git a/lib/src/layouts/paged_sliver_aligned_grid.dart b/lib/src/layouts/paged_sliver_aligned_grid.dart index 5a322d6..c4418d4 100644 --- a/lib/src/layouts/paged_sliver_aligned_grid.dart +++ b/lib/src/layouts/paged_sliver_aligned_grid.dart @@ -1,10 +1,9 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_grid.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; -import 'package:infinite_scroll_pagination/src/layouts/paged_sliver_masonry_grid.dart'; +import 'package:infinite_scroll_pagination/src/helpers/flutter_staggered_grid_view.dart'; /// A [SliverAlignedGrid] with pagination capabilities. /// diff --git a/lib/src/layouts/paged_sliver_masonry_grid.dart b/lib/src/layouts/paged_sliver_masonry_grid.dart index 7c8a935..2db4362 100644 --- a/lib/src/layouts/paged_sliver_masonry_grid.dart +++ b/lib/src/layouts/paged_sliver_masonry_grid.dart @@ -1,13 +1,9 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; import 'package:infinite_scroll_pagination/src/helpers/appended_sliver_grid.dart'; import 'package:infinite_scroll_pagination/src/base/paged_layout_builder.dart'; - -typedef SliverSimpleGridDelegateBuilder = SliverSimpleGridDelegate Function( - int childCount, -); +import 'package:infinite_scroll_pagination/src/helpers/flutter_staggered_grid_view.dart'; /// A [SliverMasonryGrid] with pagination capabilities. /// From ee3a136a47c3edca4098e88bbcb902e4cad33c08 Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 10 Jan 2025 04:11:18 +0100 Subject: [PATCH 20/37] docs: update readme example --- README.md | 65 ++++++++++++++++++------------------------------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index d0cfda0..f6931d8 100644 --- a/README.md +++ b/README.md @@ -31,59 +31,36 @@ Designed to feel like part of the Flutter framework. ## Usage ```dart -class BeerListView extends StatefulWidget { - @override - _BeerListViewState createState() => _BeerListViewState(); -} - -class _BeerListViewState extends State { - static const _pageSize = 20; - - final PagingController _pagingController = - PagingController(firstPageKey: 0); +class ListViewScreen extends StatefulWidget { + const ListViewScreen({super.key}); @override - void initState() { - super.initState(); - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); - }); - } - - Future _fetchPage(int pageKey) async { - try { - final newItems = await RemoteApi.getBeerList(pageKey, _pageSize); - final isLastPage = newItems.length < _pageSize; - if (isLastPage) { - _pagingController.appendLastPage(newItems); - } else { - final nextPageKey = pageKey + newItems.length; - _pagingController.appendPage(newItems, nextPageKey); - } - } catch (error) { - _pagingController.error = error; - } - } + State createState() => _ListViewScreenState(); +} - @override - Widget build(BuildContext context) => - // Don't worry about displaying progress or error indicators on screen; the - // package takes care of that. If you want to customize them, use the - // [PagedChildBuilderDelegate] properties. - PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - ), - ); +class _ListViewScreenState extends State { + late final _pagingController = PagingController( + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, + fetchPage: (pageKey) => RemoteApi.getPhotos(pageKey), + ); @override void dispose() { _pagingController.dispose(); super.dispose(); } + + @override + Widget build(BuildContext context) => PagingListener( + controller: _pagingController, + builder: (context, state, fetchNextPage) => PagedListView( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + ), + ); } ``` From 5982287d18cab04c8fb67834eed0ed75d2f0775f Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 10 Jan 2025 04:32:59 +0100 Subject: [PATCH 21/37] docs: reformat changelog --- CHANGELOG.md | 67 +++++++++++++++++----------------------------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78c0f07..4751666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Removed - `pubspec.lock` from version control. -## [4.0.0] - 2023-08-17 +## 4.0.0 - 2023-08-17 ### Added - [PagedMasonryGridView](https://pub.dev/documentation/infinite_scroll_pagination/4.0.0/infinite_scroll_pagination/PagedMasonryGridView-class.html). - [PagedPageView](https://pub.dev/documentation/infinite_scroll_pagination/4.0.0/infinite_scroll_pagination/PagedPageView-class.html). @@ -17,19 +17,19 @@ ### Changed - Renames `PagedSliverBuilder` to [PagedLayoutBuilder](https://pub.dev/documentation/infinite_scroll_pagination/4.0.0/infinite_scroll_pagination/PagedLayoutBuilder-class.html). -## [3.2.0] - 2022-05-23 +## 3.2.0 - 2022-05-23 ### Changed - Migrates to Flutter 3. -## [3.1.0] - 2021-07-04 +## 3.1.0 - 2021-07-04 ### Added - [animated status transitions](https://pub.dev/packages/infinite_scroll_pagination/example#animating-status-transitions). -## [3.0.1+1] - 2021-05-23 +## 3.0.1+1 - 2021-05-23 ### Added - [Flutter Favorite](https://flutter.dev/docs/development/packages-and-plugins/favorites) status to the README. -## [3.0.1] - 2021-03-08 +## 3.0.1 - 2021-03-08 ### Added - New unit tests. @@ -39,17 +39,17 @@ ### Fixed - Code formatting in `ListenableListener`. -## [3.0.0] - 2021-03-04 +## 3.0.0 - 2021-03-04 ### Changed - Promotes null safety to stable release. - Migrates example project to null safety. - Migrates code samples to null safety. -## [3.0.0-nullsafety.0] - 2021-02-06 +## 3.0.0-nullsafety.0 - 2021-02-06 ### Changed - Migrates to null safety. -## [2.3.0] - 2021-01-15 +## 2.3.0 - 2021-01-15 ### Added - [alternative constructor](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController/PagingController.fromValue.html) to [PagingController](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController-class.html) receiving an initial [PagingState](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingState-class.html). @@ -57,19 +57,19 @@ - Cookbook file name. - LICENSE file. -## [2.2.4] - 2021-01-08 +## 2.2.4 - 2021-01-08 ### Fixed - New page requests happening before the end of the current frame. -## [2.2.3] - 2020-12-14 +## 2.2.3 - 2020-12-14 ### Fixed - Bug in which manually resetting to a previous page would stop requesting subsequent pages. -## [2.2.2] - 2020-11-04 +## 2.2.2 - 2020-11-04 ### Added - Condition to avoid requesting the first page when there are preloaded items. -## [2.2.1] - 2020-10-21 +## 2.2.1 - 2020-10-21 ### Added - `shrinkWrapFirstPageIndicators` property to [PagedSliverList](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverList-class.html), [PagedSliverGrid](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverGrid-class.html), and [PagedSliverBuilder](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverBuilder-class.html). @@ -79,73 +79,50 @@ ### Fixed - Separator being displayed on completed lists. -## [2.2.0+1] - 2020-10-19 +## 2.2.0+1 - 2020-10-19 ### Changed - Constraints the Flutter SDK dependency to a minimum version of 1.22.0. -## [2.2.0] - 2020-10-18 +## 2.2.0 - 2020-10-18 ### Added - New constructor parameters from [ScrollView](https://api.flutter.dev/flutter/widgets/ScrollView-class.html) to [PagedListView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedListView-class.html) and [PagedGridView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedGridView-class.html). -## [2.1.0+1] - 2020-10-13 +## 2.1.0+1 - 2020-10-13 ### Added - Link to [raywenderlich.com tutorial](https://www.raywenderlich.com/265121/infinite-scrolling-pagination-in-flutter). ### Changed - Examples to async/await. -## [2.1.0] - 2020-10-10 +## 2.1.0 - 2020-10-10 ### Added - [noMoreItemsIndicatorBuilder](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedChildBuilderDelegate/noMoreItemsIndicatorBuilder.html) to [PagedChildBuilderDelegate](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedChildBuilderDelegate-class.html). - Properties to both grid widgets to let you choose whether to display the progress, error, and completed listing indicators as grid items or below the grid, as in the list widgets. -## [2.0.1] - 2020-10-03 +## 2.0.1 - 2020-10-03 ### Fixed - [PagingController](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController-class.html) not calling its status listeners. -## [2.0.0] - 2020-10-02 +## 2.0.0 - 2020-10-02 ### Changed - **BREAKING CHANGE**: Replaces [PagedDataSource](https://pub.dev/documentation/infinite_scroll_pagination/1.1.1/infinite_scroll_pagination/PagedDataSource-class.html) and [PagedStateChangeListener](https://pub.dev/documentation/infinite_scroll_pagination/1.1.1/infinite_scroll_pagination/PagedStateChangeListener-class.html) with [PagingController](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController-class.html), favoring composition over inheritance. -## [1.1.1] - 2020-09-23 +## 1.1.1 - 2020-09-23 ### Removed - Scroll from first page progress indicator, first page error indicator, and no items found indicator. -## [1.1.0] - 2020-09-18 +## 1.1.0 - 2020-09-18 ### Added - [PagedStateChangeListener](https://pub.dev/documentation/infinite_scroll_pagination/1.1.0/infinite_scroll_pagination/PagedStateChangeListener-class.html). -## [1.0.0+2] - 2020-08-22 +## 1.0.0+2 - 2020-08-22 ### Added - Documentation to `PagedDataSource` properties. ### Changed - README images reference URL. -## [1.0.0+1] - 2020-08-22 +## 1.0.0+1 - 2020-08-22 ### Added - Images to README.md. - Initial release. - -[4.0.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/3.2.0..4.0.0 -[3.2.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/3.1.0..3.2.0 -[3.1.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/3.0.1+1..3.1.0 -[3.0.1+1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/3.0.1..3.0.1+1 -[3.0.1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/3.0.0..3.0.1 -[3.0.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/3.0.0-nullsafety.0..3.0.0 -[3.0.0-nullsafety.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.3.0..3.0.0-nullsafety.0 -[2.3.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.2.4..2.3.0 -[2.2.4]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.2.3..2.2.4 -[2.2.3]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.2.2..2.2.3 -[2.2.2]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.2.1..2.2.2 -[2.2.1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.2.0+1..2.2.1 -[2.2.0+1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.2.0..2.2.0+1 -[2.2.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.1.0+1..2.2.0 -[2.1.0+1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.1.0..2.1.0+1 -[2.1.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.0.1..2.1.0 -[2.0.1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/2.0.0..2.0.1 -[2.0.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/1.1.1..2.0.0 -[1.1.1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/1.1.0..1.1.1 -[1.1.0]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/1.0.0+2..1.1.0 -[1.0.0+2]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/1.0.0+1..1.0.0+2 -[1.0.0+1]: https://github.com/EdsonBueno/infinite_scroll_pagination/compare/1.0.0..1.0.0+1 From 26cbab0834ba0c35f9e3a1c218127d5d7a5c304e Mon Sep 17 00:00:00 2001 From: clragon Date: Fri, 10 Jan 2025 05:36:43 +0100 Subject: [PATCH 22/37] docs: update cookbook --- example/example.md | 634 +++++++++++++++++++++------------------------ 1 file changed, 294 insertions(+), 340 deletions(-) diff --git a/example/example.md b/example/example.md index 54d0501..abdcbce 100644 --- a/example/example.md +++ b/example/example.md @@ -1,122 +1,209 @@ # Cookbook -All the snippets are from the [example project](https://github.com/EdsonBueno/infinite_scroll_pagination/tree/master/example). +More extensive examples can be found in the [example project](https://github.com/EdsonBueno/infinite_scroll_pagination/tree/master/example). -## Simple Usage +## Using PagingController + +PagingController is the out-of-the-box solution that comes with the package for managing the PagingState. Using a PagingListener, we connect it to the Paged Widget and we're good to go. You can extend the class to add features you might need, such as filtering, sorting, etc. It can also be connected to multiple Paged Widgets at the same time. ```dart -class BeerListView extends StatefulWidget { +class _ExampleScreenState extends State { + late final _pagingController = PagingController( + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, + fetchPage: (pageKey) => RemoteApi.getPhotos(pageKey), + ); + @override - _BeerListViewState createState() => _BeerListViewState(); + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => PagingListener( + controller: _pagingController, + builder: (context, state, fetchNextPage) => PagedListView( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + ), + ); } +``` -class _BeerListViewState extends State { - static const _pageSize = 20; +## Using setState - final PagingController _pagingController = - PagingController(firstPageKey: 0); +You can manually manage the PagingState using setState. This is more straightforward when you require more control over your state. - @override - void initState() { - super.initState(); - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); +```dart +class _ExampleScreenState extends State { + PagingState _state = PagingState(); + + void _fetchNextPage() async { + if (_state.isLoading) return; + + await Future.value(); + + setState(() { + _state = _state.copyWith(isLoading: true, error: null); }); - } - Future _fetchPage(int pageKey) async { try { - final newItems = await RemoteApi.getBeerList(pageKey, _pageSize); - final isLastPage = newItems.length < _pageSize; - if (isLastPage) { - _pagingController.appendLastPage(newItems); - } else { - final nextPageKey = pageKey + newItems.length; - _pagingController.appendPage(newItems, nextPageKey); - } + final newKey = (_state.keys?.last ?? 0) + 1; + final newItems = await RemoteApi.getPhotos(newKey); + final isLastPage = newItems.isEmpty; + + setState(() { + _state = _state.copyWith( + pages: [...?_state.pages, newItems], + keys: [...?_state.keys, newKey], + hasNextPage: !isLastPage, + isLoading: false, + ); + }); } catch (error) { - _pagingController.error = error; + setState(() { + _state = _state.copyWith( + error: error, + isLoading: false, + ); + }); } } @override - Widget build(BuildContext context) => - PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - ), - ); + Widget build(BuildContext context) => PagedListView( + state: _state, + fetchNextPage: _fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + ); +} +``` + +## Using a custom State Management + +You can use any state managment approach you prefer. +The only requirements for the Paged Widget to work are that you provide a PagingState and a function to fetch the next page. +Here is an example in flutter_bloc: + +```dart +sealed class PhotoEvent {} + +final class FetchNextPhotoPage extends PhotoEvent {} + +class PhotoBoc extends Bloc> { + PhotoBoc() : super(PagingState()) { + on((event, emit) { + final state = state; + if (state.isLoading) return; + + emit(state.copyWith(isLoading: true, error: null)); + + try { + final newKey = (state.keys?.last ?? 0) + 1; + final newItems = await RemoteApi.getPhotos(newKey); + final isLastPage = newItems.isEmpty; + + emit(state.copyWith( + pages: [...?state.pages, newItems], + keys: [...?state.keys, newKey], + hasNextPage: !isLastPage, + isLoading: false, + )); + } catch (error) { + emit(state.copyWith( + error: error, + isLoading: false, + )); + } + }, + ); + } +} +``` + +and then in your screen: + +```dart +class _ExampleScreenState extends State { + final _bloc = PhotoBloc(); @override void dispose() { - _pagingController.dispose(); + _bloc.dispose(); super.dispose(); } + + @override + Widget build(BuildContext context) => BlocBuilder>( + bloc: _bloc, + builder: (context, state) => PagedListView( + state: state, + fetchNextPage: _bloc.fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + ), + ); } ``` ## Customizing Indicators +You can customize the indicators by providing your own widgets to the builderDelegate. The package comes with default indicators in english. + ```dart -@override -Widget build(BuildContext context) => - PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - firstPageErrorIndicatorBuilder: (_) => FirstPageErrorIndicator( - error: _pagingController.error, - onTryAgain: () => _pagingController.refresh(), - ), - newPageErrorIndicatorBuilder: (_) => NewPageErrorIndicator( - error: _pagingController.error, - onTryAgain: () => _pagingController.retryLastFailedRequest(), - ), - firstPageProgressIndicatorBuilder: (_) => FirstPageProgressIndicator(), - newPageProgressIndicatorBuilder: (_) => NewPageProgressIndicator(), - noItemsFoundIndicatorBuilder: (_) => NoItemsFoundIndicator(), - noMoreItemsIndicatorBuilder: (_) => NoMoreItemsIndicator(), - ), - ); +PagedListView( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + firstPageErrorIndicatorBuilder: (_) => FirstPageErrorIndicator( + error: state.error, + onTryAgain: () => fetchNextPage(), + ), + newPageErrorIndicatorBuilder: (_) => NewPageErrorIndicator( + error: state.error, + onTryAgain: () => fetchNextPage(), + ), + firstPageProgressIndicatorBuilder: (_) => FirstPageProgressIndicator(), + newPageProgressIndicatorBuilder: (_) => NewPageProgressIndicator(), + noItemsFoundIndicatorBuilder: (_) => NoItemsFoundIndicator(), + noMoreItemsIndicatorBuilder: (_) => NoMoreItemsIndicator(), + ), +); ``` ## Animating Status Transitions ```dart -@override -Widget build(BuildContext context) => - PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - animateTransitions: true, - // [transitionDuration] has a default value of 250 milliseconds. - transitionDuration: const Duration(milliseconds: 500), - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - ), - ); +PagedListView( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + animateTransitions: true, + // [transitionDuration] has a default value of 250 milliseconds. + transitionDuration: const Duration(milliseconds: 500), + ), +); ``` ## Separators ```dart -@override -Widget build(BuildContext context) => - PagedListView.separated( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - ), - separatorBuilder: (context, index) => const Divider(), - ); +PagedListView.separated( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + separatorBuilder: (context, index) => const Divider(), +); ``` Works for both [PagedListView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedListView-class.html) and [PagedSliverList](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverList-class.html). @@ -126,21 +213,18 @@ Works for both [PagedListView](https://pub.dev/documentation/infinite_scroll_pag Wrap your [PagedListView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedListView-class.html), [PagedGridView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedGridView-class.html) or [CustomScrollView](https://api.flutter.dev/flutter/widgets/CustomScrollView-class.html) with a [RefreshIndicator](https://api.flutter.dev/flutter/material/RefreshIndicator-class.html) (from the [material library](https://api.flutter.dev/flutter/material/material-library.html)) and inside [onRefresh](https://api.flutter.dev/flutter/material/RefreshIndicator/onRefresh.html), call `refresh` on your [PagingController](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController-class.html): ```dart -@override -Widget build(BuildContext context) => - RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - ), - ), - ); +RefreshIndicator( + onRefresh: () => Future.sync( + () => refresh(), + ), + child: PagedListView( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + ), +); ``` ## Preceding/Following Items @@ -150,96 +234,71 @@ If you need to place some widgets before or after your list, and expect them to **Infinite Scroll Pagination** comes with [PagedSliverList](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverList-class.html) and [PagedSliverGrid](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverGrid-class.html), which works almost the same as [PagedListView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedListView-class.html) or [PagedGridView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedGridView-class.html), except that they need to be wrapped by a [CustomScrollView](https://api.flutter.dev/flutter/widgets/CustomScrollView-class.html). That allows you to give them siblings, for example: ```dart -@override -Widget build(BuildContext context) => - CustomScrollView( - slivers: [ - BeerSearchInputSliver( - onChanged: _updateSearchTerm, - ), - PagedSliverList( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), - ), - ), - ], - ); +CustomScrollView( + slivers: [ + SearchInputSliver( + onChanged: updateSearchTerm, + ), + PagedSliverList( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + ), + ), + ], +); ``` -Notice that your preceding/following widgets should also be [Sliver](https://flutter.dev/docs/development/ui/advanced/slivers)s. `BeerSearchInputSliver`, for example, is nothing but a [TextField](https://api.flutter.dev/flutter/material/TextField-class.html) wrapped by a [SliverToBoxAdapter](https://api.flutter.dev/flutter/widgets/SliverToBoxAdapter-class.html). +Notice that your preceding/following widgets should also be [Sliver](https://flutter.dev/docs/development/ui/advanced/slivers)s. `SearchInputSliver`, for example, is nothing but a [TextField](https://api.flutter.dev/flutter/material/TextField-class.html) wrapped by a [SliverToBoxAdapter](https://api.flutter.dev/flutter/widgets/SliverToBoxAdapter-class.html). ## Searching/Filtering/Sorting -There are many ways to integrate searching/filtering/sorting with this package. The best one depends on you state management approach. Below you can see a simple example for a vanilla approach: +There are many ways to integrate searching/filtering/sorting with this package. The best one depends on you state management approach. +Below you can see a very simple example: ```dart -class BeerSliverList extends StatefulWidget { - @override - _BeerSliverListState createState() => _BeerSliverListState(); -} - -class _BeerSliverListState extends State { - static const _pageSize = 17; +class _ExampleScreenState extends State { + String? _searchTerm; - final PagingController _pagingController = - PagingController(firstPageKey: 0); + late final _pagingController = PagingController( + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, + fetchPage: (pageKey) { + final results = RemoteApi.getPhotos(pageKey); - String? _searchTerm; + return _searchTerm == null + ? results + : results.where((photo) => photo.title.contains(_searchTerm!)).toList(); + }, + ); - @override - void initState() { - super.initState(); - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); - }); + void _updateSearchTerm(String searchTerm) { + _searchTerm = searchTerm; + _pagingController.refresh(); } - Future _fetchPage(pageKey) async { - try { - final newItems = await RemoteApi.getBeerList( - pageKey, - _pageSize, - searchTerm: _searchTerm, - ); - - final isLastPage = newItems.length < _pageSize; - if (isLastPage) { - _pagingController.appendLastPage(newItems); - } else { - final nextPageKey = pageKey + newItems.length; - _pagingController.appendPage(newItems, nextPageKey); - } - } catch (error) { - _pagingController.error = error; - } + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) => CustomScrollView( - slivers: [ - BeerSearchInputSliver( + slivers: [ + SearchInputSliver( onChanged: _updateSearchTerm, ), - PagedSliverList( + PagedSliverList( pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerListItem( - beer: item, - ), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), ), ), ], ); - void _updateSearchTerm(String searchTerm) { - _searchTerm = searchTerm; - _pagingController.refresh(); - } - @override void dispose() { _pagingController.dispose(); @@ -258,7 +317,7 @@ If you want to change that, and instead display the items _below_ the grid, as i ```dart @override Widget build(BuildContext context) => - PagedGridView( + PagedGridView( showNewPageProgressIndicatorAsGridChild: false, showNewPageErrorIndicatorAsGridChild: false, showNoMoreItemsIndicatorAsGridChild: false, @@ -269,129 +328,27 @@ Widget build(BuildContext context) => mainAxisSpacing: 10, crossAxisCount: 3, ), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerGridItem( - beer: item, - ), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), ), ); ``` -## Listening to Status Changes - -If you need to execute some custom action when the list status changes, such as displaying a dialog/snackbar/toast, or sending a custom event to a BLoC or so, add a status listener to your [PagingController](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController-class.html). For example: - -```dart -@override -void initState() { - super.initState(); - - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); - }); - - _pagingController.addStatusListener((status) { - if (status == PagingStatus.subsequentPageError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text( - 'Something went wrong while fetching a new page.', - ), - action: SnackBarAction( - label: 'Retry', - onPressed: () => _pagingController.retryLastFailedRequest(), - ), - ), - ); - } - }); -} -``` - ## Changing the Invisible Items Threshold -By default, the package asks a new page when there are 3 invisible items left while the user is scrolling. You can change that number in the [PagingController](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagingController-class.html)'s constructor. - -```dart -final PagingController _pagingController = - PagingController(firstPageKey: 0, invisibleItemsThreshold: 5); -``` - -## BLoC - -**Infinite Scroll Pagination** is designed to work with any state management approach you prefer in any way you'd like. Because of that, for each approach, there's not only one, but several ways in which you could work with this package. -Below you can see one of the possible ways to integrate it with BLoCs: +By default, the package asks a new page when there are 3 invisible items left while the user is scrolling. You can change that number in the [PagedChildBuilderDelegate](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedChildBuilderDelegate-class.html). ```dart -class BeerSliverGrid extends StatefulWidget { - @override - _BeerSliverGridState createState() => _BeerSliverGridState(); -} - -class _BeerSliverGridState extends State { - final BeerSliverGridBloc _bloc = BeerSliverGridBloc(); - final PagingController _pagingController = - PagingController(firstPageKey: 0); - late StreamSubscription _blocListingStateSubscription; - - @override - void initState() { - super.initState(); - - _pagingController.addPageRequestListener((pageKey) { - _bloc.onPageRequestSink.add(pageKey); - }); - - // We could've used StreamBuilder, but that would unnecessarily recreate - // the entire [PagedSliverGrid] every time the state changes. - // Instead, handling the subscription ourselves and updating only the - // _pagingController is more efficient. - _blocListingStateSubscription = - _bloc.onNewListingState.listen((listingState) { - _pagingController.value = PagingState( - nextPageKey: listingState.nextPageKey, - error: listingState.error, - itemList: listingState.itemList, - ); - }); - } - - @override - Widget build(BuildContext context) => - CustomScrollView( - slivers: [ - BeerSearchInputSliver( - onChanged: (searchTerm) => - _bloc.onSearchInputChangedSink.add(searchTerm), - ), - PagedSliverGrid( - pagingController: _pagingController, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 100 / 150, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - crossAxisCount: 3, - ), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => BeerGridItem( - beer: item, - ), - ), - ), - ], - ); - - @override - void dispose() { - _pagingController.dispose(); - _blocListingStateSubscription.cancel(); - super.dispose(); - } -} +PagedListView( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item: item), + invisibleItemsThreshold: 5, + ), +); ``` -Check out the [example project](https://github.com/EdsonBueno/infinite_scroll_pagination/tree/master/example) for the complete source code. - ## Custom Layout In case [PagedListView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedListView-class.html), [PagedSliverList](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverList-class.html), [PagedGridView](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedGridView-class.html) and [PagedSliverGrid](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverGrid-class.html) doesn't work for you, you should create a new paged layout. @@ -399,72 +356,69 @@ In case [PagedListView](https://pub.dev/documentation/infinite_scroll_pagination Creating a new layout is just a matter of using [PagedLayoutBuilder](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedLayoutBuilder-class.html) and provide it builders for the completed, in progress with error and in progress with loading layouts. For example, take a look at how [PagedSliverGrid](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverGrid-class.html) is built: ```dart -@override - @override - Widget build(BuildContext context) => - PagedLayoutBuilder( - layoutProtocol: PagedLayoutProtocol.sliver, - pagingController: pagingController, - builderDelegate: builderDelegate, - completedListingBuilder: ( - context, - itemBuilder, - itemCount, - noMoreItemsIndicatorBuilder, - ) => - AppendedSliverGrid( - sliverGridBuilder: (_, delegate) => SliverGrid( - delegate: delegate, - gridDelegate: gridDelegate, - ), - itemBuilder: itemBuilder, - itemCount: itemCount, - appendixBuilder: noMoreItemsIndicatorBuilder, - showAppendixAsGridChild: showNoMoreItemsIndicatorAsGridChild, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addSemanticIndexes: addSemanticIndexes, - addRepaintBoundaries: addRepaintBoundaries, - ), - loadingListingBuilder: ( - context, - itemBuilder, - itemCount, - progressIndicatorBuilder, - ) => - AppendedSliverGrid( - sliverGridBuilder: (_, delegate) => SliverGrid( - delegate: delegate, - gridDelegate: gridDelegate, - ), - itemBuilder: itemBuilder, - itemCount: itemCount, - appendixBuilder: progressIndicatorBuilder, - showAppendixAsGridChild: showNewPageProgressIndicatorAsGridChild, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addSemanticIndexes: addSemanticIndexes, - addRepaintBoundaries: addRepaintBoundaries, - ), - errorListingBuilder: ( - context, - itemBuilder, - itemCount, - errorIndicatorBuilder, - ) => - AppendedSliverGrid( - sliverGridBuilder: (_, delegate) => SliverGrid( - delegate: delegate, - gridDelegate: gridDelegate, - ), - itemBuilder: itemBuilder, - itemCount: itemCount, - appendixBuilder: errorIndicatorBuilder, - showAppendixAsGridChild: showNewPageErrorIndicatorAsGridChild, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addSemanticIndexes: addSemanticIndexes, - addRepaintBoundaries: addRepaintBoundaries, - ), - shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators, - ); +PagedLayoutBuilder( + layoutProtocol: PagedLayoutProtocol.sliver, + pagingController: pagingController, + builderDelegate: builderDelegate, + completedListingBuilder: ( + context, + itemBuilder, + itemCount, + noMoreItemsIndicatorBuilder, + ) => + AppendedSliverGrid( + sliverGridBuilder: (_, delegate) => SliverGrid( + delegate: delegate, + gridDelegate: gridDelegate, + ), + itemBuilder: itemBuilder, + itemCount: itemCount, + appendixBuilder: noMoreItemsIndicatorBuilder, + showAppendixAsGridChild: showNoMoreItemsIndicatorAsGridChild, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addSemanticIndexes: addSemanticIndexes, + addRepaintBoundaries: addRepaintBoundaries, + ), + loadingListingBuilder: ( + context, + itemBuilder, + itemCount, + progressIndicatorBuilder, + ) => + AppendedSliverGrid( + sliverGridBuilder: (_, delegate) => SliverGrid( + delegate: delegate, + gridDelegate: gridDelegate, + ), + itemBuilder: itemBuilder, + itemCount: itemCount, + appendixBuilder: progressIndicatorBuilder, + showAppendixAsGridChild: showNewPageProgressIndicatorAsGridChild, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addSemanticIndexes: addSemanticIndexes, + addRepaintBoundaries: addRepaintBoundaries, + ), + errorListingBuilder: ( + context, + itemBuilder, + itemCount, + errorIndicatorBuilder, + ) => + AppendedSliverGrid( + sliverGridBuilder: (_, delegate) => SliverGrid( + delegate: delegate, + gridDelegate: gridDelegate, + ), + itemBuilder: itemBuilder, + itemCount: itemCount, + appendixBuilder: errorIndicatorBuilder, + showAppendixAsGridChild: showNewPageErrorIndicatorAsGridChild, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addSemanticIndexes: addSemanticIndexes, + addRepaintBoundaries: addRepaintBoundaries, + ), + shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators, +); ``` Note the usage of [PagedLayoutProtocol.sliver](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedLayoutProtocol/sliver.html) which tells the package that the layout is a [Sliver](https://flutter.dev/docs/development/ui/advanced/slivers). From df0903981f08281a0d59b051b5ee73d80c15e7ca Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 27 Jan 2025 23:15:00 +0100 Subject: [PATCH 23/37] fix: rebuild during build when calling fetch next page --- lib/src/core/paging_controller.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index 68ad1db..4508f62 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -53,6 +53,10 @@ class PagingController final operation = this.operation = Object(); + // We wait a single ticket to prevent rebuilding during a build. + // Preferably, we wouldn't need to do this, but it is unclear how to avoid it. + await Future.value(null); + value = value.copyWith( isLoading: true, error: null, From 1edeae51a958c1e07416d9fe216cc75bccb81e6f Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 27 Jan 2025 23:50:36 +0100 Subject: [PATCH 24/37] fix: refresh cancelling previous call --- lib/src/core/paging_controller.dart | 4 +-- test/core/paging_controller_test.dart | 47 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index 4508f62..77a7a22 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -88,8 +88,6 @@ class PagingController newItems = fetchResult; } - if (operation != operation) return; - state = state.copyWith( pages: [...?state.pages, newItems], keys: [...?state.keys, nextPageKey], @@ -104,8 +102,8 @@ class PagingController rethrow; } } finally { - value = state.copyWith(isLoading: false); if (operation == this.operation) { + value = state.copyWith(isLoading: false); this.operation = null; } } diff --git a/test/core/paging_controller_test.dart b/test/core/paging_controller_test.dart index d931fb4..ebbecad 100644 --- a/test/core/paging_controller_test.dart +++ b/test/core/paging_controller_test.dart @@ -123,6 +123,53 @@ void main() { expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.error, isNull); }); + + test('cancels previous refresh', () async { + bool hasBeenCalled = false; + final completer1 = Completer>(); + final completer2 = Completer>(); + + bool hasFailed = false; + + pagingController = PagingController( + getNextPageKey: (state) => nextPageKey, + fetchPage: (_) { + if (hasBeenCalled) { + return completer2.future; + } else { + hasBeenCalled = true; + return completer1.future; + } + }); + + final wrongItems = ['Wrong Item 1', 'Wrong Item 2']; + + pagingController.addListener(() { + try { + expect(pagingController.value.pages, isNot([wrongItems])); + } catch (e) { + hasFailed = true; + } + }); + + pagingController.fetchNextPage(); + + await Future.delayed(Duration.zero); + + pagingController.refresh(); + pagingController.fetchNextPage(); + + await Future.delayed(Duration.zero); + + completer1.complete(wrongItems); + completer2.complete(fetchedItems); + + await Future.delayed(Duration.zero); + + expect(pagingController.value.isLoading, isFalse); + expect(pagingController.value.pages, [fetchedItems]); + expect(hasFailed, isFalse); + }); }); group('cancel', () { From 262781a7924037f4b814efccf35957e21da9b491 Mon Sep 17 00:00:00 2001 From: clragon Date: Tue, 28 Jan 2025 22:23:04 +0100 Subject: [PATCH 25/37] fix: invisibleItemsThreshold off-by-one --- lib/src/base/paged_layout_builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index d4e082a..aea19ca 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -246,7 +246,7 @@ class _PagedLayoutBuilderState Date: Wed, 29 Jan 2025 00:01:02 +0100 Subject: [PATCH 26/37] refactor: improve PagingStateBase constructor --- lib/src/core/paging_state.dart | 14 ++++++---- lib/src/core/paging_state_base.dart | 43 ++++++++++------------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/lib/src/core/paging_state.dart b/lib/src/core/paging_state.dart index 7889005..721352b 100644 --- a/lib/src/core/paging_state.dart +++ b/lib/src/core/paging_state.dart @@ -42,15 +42,15 @@ abstract class PagingState copyWith({ - FutureOr>?>? pages = const Omit(), - FutureOr?>? keys = const Omit(), - FutureOr? error = const Omit(), - FutureOr? hasNextPage = const Omit(), - FutureOr? isLoading = const Omit(), + Defaulted>?>? pages = const Omit(), + Defaulted?>? keys = const Omit(), + Defaulted? error = const Omit(), + Defaulted? hasNextPage = const Omit(), + Defaulted? isLoading = const Omit(), }); /// Returns a copy this [PagingState] but @@ -70,6 +70,8 @@ extension ItemListExtension List? get items => pages?.expand((e) => e).toList(); } +typedef Defaulted = FutureOr; + /// Sentinel value to omit a parameter from a copyWith call. /// This is used to distinguish between a parameter being omitted and a parameter /// being set to null. diff --git a/lib/src/core/paging_state_base.dart b/lib/src/core/paging_state_base.dart index a2ce48a..ad16ec6 100644 --- a/lib/src/core/paging_state_base.dart +++ b/lib/src/core/paging_state_base.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; @@ -10,34 +8,23 @@ import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; /// all of its fields are deeply equal. base class PagingStateBase implements PagingState { - factory PagingStateBase({ + /// Creates a [PagingStateBase] with the given parameters. + /// + /// Ensures that [pages] and [keys] are unmodifiable lists. + PagingStateBase({ List>? pages, List? keys, - Object? error, - bool hasNextPage = true, - bool isLoading = false, - }) => - PagingStateBase._( - pages: switch (pages) { + this.error, + this.hasNextPage = true, + this.isLoading = false, + }) : pages = switch (pages) { null => null, _ => List.unmodifiable(pages), }, - keys: switch (keys) { + keys = switch (keys) { null => null, _ => List.unmodifiable(keys), - }, - error: error, - hasNextPage: hasNextPage, - isLoading: isLoading, - ); - - const PagingStateBase._({ - required this.pages, - required this.keys, - required this.error, - required this.hasNextPage, - required this.isLoading, - }); + }; @override final List>? pages; @@ -56,11 +43,11 @@ base class PagingStateBase @override PagingState copyWith({ - FutureOr>?>? pages = const Omit(), - FutureOr?>? keys = const Omit(), - FutureOr? error = const Omit(), - FutureOr? hasNextPage = const Omit(), - FutureOr? isLoading = const Omit(), + Defaulted>?>? pages = const Omit(), + Defaulted?>? keys = const Omit(), + Defaulted? error = const Omit(), + Defaulted? hasNextPage = const Omit(), + Defaulted? isLoading = const Omit(), }) => PagingStateBase( pages: pages is Omit ? this.pages : pages as List>?, From 6b12b3ffad2c61699465a7f273a3b127b65dc69a Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 29 Jan 2025 00:52:12 +0100 Subject: [PATCH 27/37] docs: add migration guide --- CHANGELOG.md | 19 ++++++ MIGRATION.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++ 3 files changed, 196 insertions(+) create mode 100644 MIGRATION.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4751666..5ea9fd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## UNRELEASED +### Added +- `PagingListener` widget to connect a `PagingController` to a `PagedLayoutBuilder`. + +### Changed +- `PagingController` no longer has `addPageRequestListener` method and `firstPageKey` parameter. Use the `fetchPage` and `getNextPageKey` parameters of the constructor instead. +- `PagingController` no longer has the `itemList`, `error`, and `nextPageKey` getters and setters. All values are now stored in `PagingState`. +- `PagingController` no longer has the `appendPage`, `appendLastPage`, and `retryLastFailedRequest` methods. Use the `copyWith` method of `PagingState` to update its fields. +- `PagingController` no longer has the `invisibleItemsThreshold` field. It is now configured in `PagedChildBuilderDelegate`. +- `PagedLayoutBuilder` no longer accepts `pagingController` as a parameter. It now takes `PagingState` and `fetchNextPage` instead. +- `PagingState` now uses `pages` (`List>`) instead of `itemList` (`List`). A new extension getter `items` is provided for flattening. +- `PagingState` now features `keys`, a list storing all fetched keys, and `hasNextPage` replacing `nextPageKey`. +- `PagingState` now includes `isLoading`, which tracks whether a request is in progress. +- `PagingState` now provides `error` as type `Object?` instead of `dynamic`. + +### Fixed +- `PagingController` now deduplicates requests. +- `PagingController` refresh operations now cancel previous requests. + ## 4.1.0 - 2024-11-09 ### Added - [PagedSliverMasonryGrid](https://pub.dev/documentation/infinite_scroll_pagination/latest/infinite_scroll_pagination/PagedSliverMasonryGrid-class.html). diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..7059d2b --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,173 @@ +# From v4 to v5 + +In v5, the package decouples the PagingController from PagedLayoutBuilder and its descendants, to allow greater freedom in how a PagingState is managed. +This is a large breaking change and will require refactoring in your code. + +## Dependencies + +The package was upgraded to a newer modern flutter major version. + +- Newly requires `dart: ">=3.4.0"` and `flutter: ">=3.0.0"` +- Newly depends on `collection: ">=1.15.0"` + +## PagingController + +Since PagingController is now optional, it was changed to be more opinionated and easier to use. + +Instead of adding `PageRequestListener` to your PagingController, like so: + +```dart +final pagingController = PagingController(firstPageKey: 1); +pagingController.addPageRequestListener(fetchPage); +``` + +and manually updating the next page key: + +```dart +pagingController.appendPage(newItems, nextPageKey); +``` + +PagingController now directly takes and controls the fetching process: + +```dart +late final pagingController = PagingController( + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, + fetchPage: (pageKey) => fetchPage(pageKey), +); +``` + +This fixes several issues of the past: + +- Requests will now be actively deduplicated +- Refresh can now cancel previous requests + +The PagingController can also be arbitrarily extended to include additional functionality that you might require. +The source code explains how to structure new code. + +### API Changes + +- No longer features `itemList`, `error` and `nextPageKey` getters and setters. All values are directly stored in the `PagingState`. +- `addPageRequestListener` was removed. Use the `fetchPage` parameter of the constructor instead. +- `appendPage` and `appendLastPage` were removed. Use the `copyWith` method of the `PagingState` to update the `pages`, `keys` and `hasNextPage` fields. +- `retryLastFailedRequest` was removed. You can simply call `fetchNextPage` to try again. +- No longer accepts `invisibleItemsThreshold`. To configure the `invisibleItemsThreshold` of a layout, use the corresponding parameter of its `PagedChildBuilderDelegate`. + +## PagedLayoutBuilder + +Because the PagingController is now independant, PagedLayoutBuilder and its subclasses no longer take a controller as a parameter like so: + +```dart +PagedListView.builder( + pagingController: pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item), + ), +), +``` + +Instead, it is more agnostic: + +```dart +PagedListView.builder( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item), + ), +), +``` + +Taking in a `PagingState` and a `fetchNextPage` function. `fetchNextPage` is a void function, and does not receive a page key. + +This new design can be used in combination with any state management solution much more easily. A PagingController is no longer required. +To continue using a PagingController for its convenience, you can connect it to any number of Paged Layouts via the PagingListener: + +```dart +PagingListener( + controller: pagingController, + builder: (context, state, fetchNextPage) => + PagedListView.builder( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ImageListTile(item), + ), + ), +), +``` + +It is highly recommended to directly store a PagingState inside of your preferred state management solution, +instead of storing a PagingController, should you not wish to use the PagingController directly. + +Examples of using a custom state management solution can be found in the example project. + +### API Changes + +- No longer features `pagingController` parameter. Use the `state` and `fetchNextPage` parameters instead. +- Now uses `invisibleItemsThreshold` from `PagedChildBuilderDelegate` instead of `PagingController`. + +## PagingState + +The PagingState in v5 has been updated to be more flexible: + +- It now includes a List of all keys, `keys`, that have been fetched, each index corresponding to a page of items. +- Instead of storing the next page key, it now includes a boolean `hasNextPage` to indicate if there are more pages to fetch. +- Lastly it now also includes a loading state, in `isLoading`. + +Most users probably do not directly interact with PagingState beyond reading it. +However, the PagingState was also changed to allow customisation of its fields. + +Instead of storing your Query parameters externally, you can now extend the PagingState to include them: + +```dart +final class MyPagingState + extends PagingStateBase { + MyPagingState({ + super.pages, + // ... + this.query, + }); + + final String? query; + + @override + PagingState copyWith({ + Defaulted>?>? pages = const Omit(), + // ... + Defaulted? query = const Omit(), + }) { + final partial = super.copyWith( + pages: pages, + // ... + isLoading: isLoading, + ); + + return MyPagingState( + pages: partial.pages, + // ... + query: query is Omit ? this.query : query as String?, + ); + } + + @override + PagingState reset() { + final partial = super.reset(); + + return MyPagingState( + pages: partial.pages, + // ... + query: null, + ); + } +} +``` + +Extended PagingStates will correctly work with PagingController, thanks to the `copyWith` and `reset` methods. For a better understanding of how this works, check out the source code. + +### API Changes + +- `itemList` has been replaced by `pages`, which is List> instead of List. An extension `items` getter is provided to flatten the list. +- `keys` is a new field that stores all keys that have been fetched, each index corresponding to a page of items. +- `error` is now type Object? instead of dynamic. +- `nextPageKey` was removed. You can use the `keys` field to compute the next page and `hasNextPage` to determine if there are more pages. +- `isLoading` is a new field that indicates if a request is currently in progress. diff --git a/README.md b/README.md index f6931d8..7eb7479 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,10 @@ For more usage examples, please take a look at our [cookbook](https://pub.dev/pa - **Listen to state changes**: In addition to displaying widgets to inform the current status, such as progress and error indicators, you can also [use a listener](https://pub.dev/packages/infinite_scroll_pagination/example#listening-to-status-changes) to display dialogs/snackbars/toasts or execute any other action. +## Migration + +if you are upgrading the package, please check the [migration guide](https://github.com/EdsonBueno/infinite_scroll_pagination/tree/master/MIGRATION.md) for instructions on how to update your code. + ## API Overview

From 7e7b3ab5b98bd9494f98ed8e312e1c6f15d0cf9f Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 29 Jan 2025 02:09:56 +0100 Subject: [PATCH 28/37] fix: paging controller tests --- test/core/paging_controller_test.dart | 56 +++++++++++++++++---------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/test/core/paging_controller_test.dart b/test/core/paging_controller_test.dart index ebbecad..6c33f6e 100644 --- a/test/core/paging_controller_test.dart +++ b/test/core/paging_controller_test.dart @@ -15,7 +15,7 @@ void main() { fetchedItems = ['Item 1', 'Item 2']; getNextPageKey(state) => nextPageKey; - fetchPage(pageKey) { + List fetchPage(int pageKey) { fetchCalled = true; return fetchedItems; } @@ -30,20 +30,20 @@ void main() { test('requests the next page', () async { pagingController.fetchNextPage(); + await Future.value(null); + expect(fetchCalled, isTrue); - expect(pagingController.value.pages, [ - ['Item 1', 'Item 2'] - ]); + expect(pagingController.value.pages, [fetchedItems]); expect(pagingController.value.keys, [nextPageKey]); }); - test('fetches a page synchronously when possible', () { + test('fetches a page synchronously when possible', () async { pagingController.fetchNextPage(); + await Future.value(null); + expect(fetchCalled, isTrue); - expect(pagingController.value.pages, [ - ['Item 1', 'Item 2'] - ]); + expect(pagingController.value.pages, [fetchedItems]); expect(pagingController.value.keys, [nextPageKey]); }); @@ -58,6 +58,8 @@ void main() { pagingController.fetchNextPage(); pagingController.fetchNextPage(); + await Future.value(null); + expect(fetchCalled, isFalse); expect(pagingController.value.isLoading, isTrue); @@ -71,6 +73,8 @@ void main() { nextPageKey = null; pagingController.fetchNextPage(); + await Future.value(null); + expect(fetchCalled, isFalse); expect(pagingController.value.hasNextPage, isFalse); }); @@ -90,6 +94,8 @@ void main() { pagingController.fetchNextPage(); + await Future.value(null); + expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.error, isA()); }); @@ -102,6 +108,9 @@ void main() { expect(() async => pagingController.fetchNextPage(), throwsA(isA())); + + await Future.value(null); + expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.error, isA()); }); @@ -126,11 +135,11 @@ void main() { test('cancels previous refresh', () async { bool hasBeenCalled = false; + bool hasFailed = false; + final completer1 = Completer>(); final completer2 = Completer>(); - bool hasFailed = false; - pagingController = PagingController( getNextPageKey: (state) => nextPageKey, fetchPage: (_) { @@ -154,17 +163,17 @@ void main() { pagingController.fetchNextPage(); - await Future.delayed(Duration.zero); + await Future.value(null); pagingController.refresh(); pagingController.fetchNextPage(); - await Future.delayed(Duration.zero); + await Future.value(null); completer1.complete(wrongItems); completer2.complete(fetchedItems); - await Future.delayed(Duration.zero); + await Future.value(null); expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.pages, [fetchedItems]); @@ -174,26 +183,31 @@ void main() { group('cancel', () { test('resets state and stops fetch', () async { - final Completer> completer = Completer>(); - pagingController = PagingController( getNextPageKey: (state) => nextPageKey, - fetchPage: (_) => completer.future, + fetchPage: (page) => Future.value(['Item $page']), ); pagingController.fetchNextPage(); - expect(pagingController.value.isLoading, isTrue); + await Future.value(null); + await Future.value(null); + + expect(pagingController.value.pages, [ + ['Item 1'] + ]); + + pagingController.fetchNextPage(); + + await Future.value(null); pagingController.cancel(); - expect(pagingController.value.isLoading, isFalse); - completer.complete(fetchedItems); + await Future.value(null); - await Future.delayed(Duration.zero); expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.pages, [ - ['Item 1', 'Item 2'] + ['Item 1'] ]); }); }); From b3e9356440c17cf71131fa57eb41a999af700aa7 Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 29 Jan 2025 02:37:15 +0100 Subject: [PATCH 29/37] feat: allow modifying state during fetch --- lib/src/core/paging_controller.dart | 8 ++++++ test/core/paging_controller_test.dart | 37 ++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/src/core/paging_controller.dart b/lib/src/core/paging_controller.dart index 77a7a22..3125f9d 100644 --- a/lib/src/core/paging_controller.dart +++ b/lib/src/core/paging_controller.dart @@ -16,6 +16,10 @@ typedef FetchPageCallback /// /// This is an unopinionated controller implemented through vanilla Flutter's [ValueNotifier]. /// The controller acts as a mutex to prevent multiple fetches at the same time. +/// +/// Note that for convenience, fetch operations are not atomic. +/// The state may be updated during a fetch operation. This should be done fully synchronously, +/// as otherwise, the state may become desynchronized. class PagingController extends ValueNotifier> { PagingController({ @@ -88,6 +92,10 @@ class PagingController newItems = fetchResult; } + // Update our state in case it was modified during the fetch operation. + // This beaks atomicity, but is necessary to allow users to modify the state during a fetch. + state = value; + state = state.copyWith( pages: [...?state.pages, newItems], keys: [...?state.keys, nextPageKey], diff --git a/test/core/paging_controller_test.dart b/test/core/paging_controller_test.dart index 6c33f6e..61cd2b8 100644 --- a/test/core/paging_controller_test.dart +++ b/test/core/paging_controller_test.dart @@ -86,6 +86,41 @@ void main() { expect(fetchCalled, isFalse); }); + // We have intentionally broken atomicity of PagingController. + // This is because we want users to be able to modify their item list even during a fetch. + // It is unclear whether this will come back to bite us. + test('allows modifying state during a fetch', () async { + pagingController = PagingController( + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, + fetchPage: (page) => Future.value(['Item $page']), + ); + + pagingController.fetchNextPage(); + + await Future.value(null); + await Future.value(null); + + pagingController.fetchNextPage(); + + await Future.value(null); + + pagingController.value = pagingController.value.copyWith( + pages: pagingController.value.pages + ?.map( + (a) => a.map((b) => b.toUpperCase()).toList(), + ) + .toList(), + ); + + await Future.value(null); + + expect(pagingController.value.isLoading, isFalse); + expect(pagingController.value.pages, [ + ['ITEM 1'], + ['Item 2'], + ]); + }); + test('catches Exceptions', () async { pagingController = PagingController( getNextPageKey: (state) => nextPageKey, @@ -184,7 +219,7 @@ void main() { group('cancel', () { test('resets state and stops fetch', () async { pagingController = PagingController( - getNextPageKey: (state) => nextPageKey, + getNextPageKey: (state) => (state.keys?.last ?? 0) + 1, fetchPage: (page) => Future.value(['Item $page']), ); From 1fa050c3bc446c05141cffffd454ec0960ec3ebc Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 29 Jan 2025 03:14:00 +0100 Subject: [PATCH 30/37] feat: add extension methods --- lib/infinite_scroll_pagination.dart | 1 + lib/src/base/paged_layout_builder.dart | 1 + lib/src/core/extensions.dart | 54 ++++++++++++++++++++++++++ lib/src/core/paging_state.dart | 6 --- lib/src/core/paging_status.dart | 1 + pubspec.yaml | 1 + 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 lib/src/core/extensions.dart diff --git a/lib/infinite_scroll_pagination.dart b/lib/infinite_scroll_pagination.dart index 2a4b8ed..ccd0827 100644 --- a/lib/infinite_scroll_pagination.dart +++ b/lib/infinite_scroll_pagination.dart @@ -6,6 +6,7 @@ export 'src/core/paging_controller.dart'; export 'src/core/paging_state.dart'; export 'src/core/paging_state_base.dart'; export 'src/core/paging_status.dart'; +export 'src/core/extensions.dart'; export 'src/helpers/appended_sliver_child_builder_delegate.dart'; export 'src/helpers/appended_sliver_grid.dart'; diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index aea19ca..af6cde9 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/src/base/paged_child_builder_delegate.dart'; +import 'package:infinite_scroll_pagination/src/core/extensions.dart'; import 'package:infinite_scroll_pagination/src/defaults/first_page_error_indicator.dart'; import 'package:infinite_scroll_pagination/src/defaults/first_page_progress_indicator.dart'; diff --git a/lib/src/core/extensions.dart b/lib/src/core/extensions.dart new file mode 100644 index 0000000..a2f3f7a --- /dev/null +++ b/lib/src/core/extensions.dart @@ -0,0 +1,54 @@ +import 'package:infinite_scroll_pagination/src/core/paging_controller.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; +import 'package:infinite_scroll_pagination/src/core/paging_status.dart'; +import 'package:meta/meta.dart'; + +extension PagingStateExtension on PagingState { + /// The list of items fetched so far. A flattened version of [pages]. + List? get items => + pages != null ? List.unmodifiable(pages!.expand((e) => e)) : null; + + /// Convenience method to update the items of the state by applying a mapper function to each item. + /// + /// The result of this method is a new [PagingState] with the same properties as the original state + /// except for the items, which are replaced by the mapped items. + @UseResult('Use the returned value as your new state.') + PagingState mapItems( + ItemType Function(ItemType item) mapper, + ) => + copyWith( + pages: pages?.map((page) => page.map(mapper).toList()).toList(), + ); +} + +/// Helper extensions to quickly access the state of a [PagingController]. +extension PagingControllerExtension on PagingController { + /// The pages fetched so far. + List>? get pages => value.pages; + + /// The items fetched so far. A flattened version of [pages]. + List? get items => value.items; + + /// Convenience method to update the items of the state. + /// + /// Items cannot be directly assigned, because they are backed by a list of pages. + void mapItems(ItemType Function(ItemType item) mapper) => + value = value.mapItems(mapper); + + /// The keys of the pages fetched so far. + List? get keys => value.keys; + + /// The last error that occurred while fetching a page. + Object? get error => value.error; + + /// Will be `true` if there is a next page to be fetched. + bool get hasNextPage => value.hasNextPage; + + /// Will be `true` if a page is currently being fetched. + bool get isLoading => value.isLoading; + + /// The paging status. + PagingStatus get status => value.status; +} diff --git a/lib/src/core/paging_state.dart b/lib/src/core/paging_state.dart index 721352b..420e167 100644 --- a/lib/src/core/paging_state.dart +++ b/lib/src/core/paging_state.dart @@ -64,12 +64,6 @@ abstract class PagingState reset(); } -extension ItemListExtension - on PagingState { - /// The list of all items in the pages. - List? get items => pages?.expand((e) => e).toList(); -} - typedef Defaulted = FutureOr; /// Sentinel value to omit a parameter from a copyWith call. diff --git a/lib/src/core/paging_status.dart b/lib/src/core/paging_status.dart index bb8c23c..ab29dd7 100644 --- a/lib/src/core/paging_status.dart +++ b/lib/src/core/paging_status.dart @@ -1,3 +1,4 @@ +import 'package:infinite_scroll_pagination/src/core/extensions.dart'; import 'package:infinite_scroll_pagination/src/core/paging_state.dart'; /// All possible status for a pagination. diff --git a/pubspec.yaml b/pubspec.yaml index b16ff3d..9667f60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: flutter_staggered_grid_view: ^0.7.0 sliver_tools: ^0.2.12 collection: ">=1.15.0" + meta: ">=1.8.0" dev_dependencies: flutter_test: From 556249d5409edd162f248407aa74da4c9608cce9 Mon Sep 17 00:00:00 2001 From: clragon Date: Wed, 29 Jan 2025 21:19:00 +0100 Subject: [PATCH 31/37] feat: add assert for constructing valid paging state --- lib/src/core/paging_state_base.dart | 6 +++++- test/core/paging_status_test.dart | 4 ++++ test/utils/paging_controller_utils.dart | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/core/paging_state_base.dart b/lib/src/core/paging_state_base.dart index ad16ec6..a2c34fe 100644 --- a/lib/src/core/paging_state_base.dart +++ b/lib/src/core/paging_state_base.dart @@ -17,7 +17,11 @@ base class PagingStateBase this.error, this.hasNextPage = true, this.isLoading = false, - }) : pages = switch (pages) { + }) : assert( + pages?.length == keys?.length, + 'The length of pages and keys must be equal.', + ), + pages = switch (pages) { null => null, _ => List.unmodifiable(pages), }, diff --git a/test/core/paging_status_test.dart b/test/core/paging_status_test.dart index ccfc263..4ec9857 100644 --- a/test/core/paging_status_test.dart +++ b/test/core/paging_status_test.dart @@ -24,6 +24,7 @@ void main() { () { pagingState = PagingState( pages: const [], + keys: const [], hasNextPage: false, ); expect(pagingState.status, PagingStatus.noItemsFound); @@ -36,6 +37,7 @@ void main() { pages: const [ ['Item 1'] ], + keys: const [1], hasNextPage: true, ); expect(pagingState.status, PagingStatus.ongoing); @@ -48,6 +50,7 @@ void main() { pages: const [ ['Item 1'] ], + keys: const [1], error: Exception('Error'), hasNextPage: true, ); @@ -61,6 +64,7 @@ void main() { pages: const [ ['Item 1'] ], + keys: const [1], hasNextPage: false, ); expect(pagingState.status, PagingStatus.completed); diff --git a/test/utils/paging_controller_utils.dart b/test/utils/paging_controller_utils.dart index 8f9852d..dd90951 100644 --- a/test/utils/paging_controller_utils.dart +++ b/test/utils/paging_controller_utils.dart @@ -21,7 +21,7 @@ extension TestPagingState on PagingState { PagingState(error: TestException()); static PagingState noItemsFound() => PagingState( - pages: const [], + pages: const [[]], keys: const [1], hasNextPage: false, ); From 6ce2040900d534fa7dfd445344be43b637f57642 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 2 Feb 2025 13:51:55 +0100 Subject: [PATCH 32/37] fix: large invisible thresholds not triggering page request --- lib/src/base/paged_layout_builder.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index af6cde9..64bdea3 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -246,17 +246,21 @@ class _PagedLayoutBuilderState itemList, ) { if (!_hasRequestedNextPage) { - final newPageRequestTriggerIndex = - max(0, _itemCount - 1 - _invisibleItemsThreshold); + final maxIndex = max(0, _itemCount - 1); + final triggerIndex = max(0, maxIndex - _invisibleItemsThreshold); - final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; + // It is important to check whether we are past the trigger, not just at it. + // This is because otherwise, large tresholds will place the trigger behind the user, + // Leading to the refresh never being triggered. + // This behaviour is okay because we make sure not to excessively request pages. + final hasPassedTrigger = index >= triggerIndex; - if (_hasNextPage && isBuildingTriggerIndexItem) { + if (_hasNextPage && hasPassedTrigger) { + _hasRequestedNextPage = true; // Schedules the request for the end of this frame. WidgetsBinding.instance.addPostFrameCallback((_) { _fetchNextPage(); }); - _hasRequestedNextPage = true; } } From c26cd6f7646c70c9e2c7d4d0286d3b05cd9f635d Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 2 Feb 2025 14:02:07 +0100 Subject: [PATCH 33/37] fix: incorrectly calling fetch next page during build --- example/lib/samples/page_view.dart | 3 --- lib/src/base/paged_layout_builder.dart | 11 ++++++----- lib/src/core/paging_controller.dart | 4 ---- test/core/paging_controller_test.dart | 12 ------------ 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/example/lib/samples/page_view.dart b/example/lib/samples/page_view.dart index 816e4ae..3a37927 100644 --- a/example/lib/samples/page_view.dart +++ b/example/lib/samples/page_view.dart @@ -26,9 +26,6 @@ class _PageViewScreenState extends State { void fetchNextPage() async { if (_state.isLoading) return; - // we wait one tick to avoid calling setState at the wrong time - await Future.value(); - setState(() { // set loading to true and remove any previous error _state = _state.copyWith(isLoading: true, error: null); diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index 64bdea3..e69dd25 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -111,7 +111,11 @@ class _PagedLayoutBuilderState> { PagingState get _state => widget.state; - NextPageCallback get _fetchNextPage => widget.fetchNextPage; + NextPageCallback get _fetchNextPage => + // We make sure to only schedule the fetch after the current build is done. + // This is important to prevent recursive builds. + () => WidgetsBinding.instance + .addPostFrameCallback((_) => widget.fetchNextPage()); PagedChildBuilderDelegate get _builderDelegate => widget.builderDelegate; @@ -257,10 +261,7 @@ class _PagedLayoutBuilderState final operation = this.operation = Object(); - // We wait a single ticket to prevent rebuilding during a build. - // Preferably, we wouldn't need to do this, but it is unclear how to avoid it. - await Future.value(null); - value = value.copyWith( isLoading: true, error: null, diff --git a/test/core/paging_controller_test.dart b/test/core/paging_controller_test.dart index 61cd2b8..f26afe7 100644 --- a/test/core/paging_controller_test.dart +++ b/test/core/paging_controller_test.dart @@ -30,8 +30,6 @@ void main() { test('requests the next page', () async { pagingController.fetchNextPage(); - await Future.value(null); - expect(fetchCalled, isTrue); expect(pagingController.value.pages, [fetchedItems]); expect(pagingController.value.keys, [nextPageKey]); @@ -97,13 +95,10 @@ void main() { pagingController.fetchNextPage(); - await Future.value(null); await Future.value(null); pagingController.fetchNextPage(); - await Future.value(null); - pagingController.value = pagingController.value.copyWith( pages: pagingController.value.pages ?.map( @@ -129,8 +124,6 @@ void main() { pagingController.fetchNextPage(); - await Future.value(null); - expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.error, isA()); }); @@ -144,8 +137,6 @@ void main() { expect(() async => pagingController.fetchNextPage(), throwsA(isA())); - await Future.value(null); - expect(pagingController.value.isLoading, isFalse); expect(pagingController.value.error, isA()); }); @@ -225,7 +216,6 @@ void main() { pagingController.fetchNextPage(); - await Future.value(null); await Future.value(null); expect(pagingController.value.pages, [ @@ -234,8 +224,6 @@ void main() { pagingController.fetchNextPage(); - await Future.value(null); - pagingController.cancel(); await Future.value(null); From 551ff9f20429ebb51732ad7d47a6fd30da5bb97c Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 2 Feb 2025 14:12:43 +0100 Subject: [PATCH 34/37] fix: animate transition between first page loading, error and empty --- lib/src/base/paged_layout_builder.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/base/paged_layout_builder.dart b/lib/src/base/paged_layout_builder.dart index e69dd25..636cce1 100644 --- a/lib/src/base/paged_layout_builder.dart +++ b/lib/src/base/paged_layout_builder.dart @@ -190,16 +190,19 @@ class _PagedLayoutBuilderState _FirstPageStatusIndicatorBuilder( + key: const ValueKey(PagingStatus.loadingFirstPage), builder: _firstPageProgressIndicatorBuilder, shrinkWrap: _shrinkWrapFirstPageIndicators, layoutProtocol: _layoutProtocol, ), PagingStatus.firstPageError => _FirstPageStatusIndicatorBuilder( + key: const ValueKey(PagingStatus.firstPageError), builder: _firstPageErrorIndicatorBuilder, shrinkWrap: _shrinkWrapFirstPageIndicators, layoutProtocol: _layoutProtocol, ), PagingStatus.noItemsFound => _FirstPageStatusIndicatorBuilder( + key: const ValueKey(PagingStatus.noItemsFound), builder: _noItemsFoundIndicatorBuilder, shrinkWrap: _shrinkWrapFirstPageIndicators, layoutProtocol: _layoutProtocol, @@ -301,6 +304,7 @@ class _PagedLayoutAnimator extends StatelessWidget { class _FirstPageStatusIndicatorBuilder extends StatelessWidget { const _FirstPageStatusIndicatorBuilder({ + super.key, required this.builder, required this.layoutProtocol, this.shrinkWrap = false, From 51a31b3eabde320a8e316b84a2542ec9d036bde9 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 2 Feb 2025 14:33:25 +0100 Subject: [PATCH 35/37] feat: add item filter extension --- lib/src/core/extensions.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/src/core/extensions.dart b/lib/src/core/extensions.dart index a2f3f7a..8d97289 100644 --- a/lib/src/core/extensions.dart +++ b/lib/src/core/extensions.dart @@ -13,13 +13,29 @@ extension PagingStateExtension mapItems( ItemType Function(ItemType item) mapper, ) => copyWith( pages: pages?.map((page) => page.map(mapper).toList()).toList(), ); + + /// Convenience method to filter the items of the state by applying a predicate function to each item. + /// + /// The result of this method is a new [PagingState] with the same properties as the original state + /// except for the items, which are replaced by the filtered items. + /// + /// It is not recommended to reassign the result of this method back to a state variable, because + /// the filtered items will be lost. Instead, use the returned value as computed state only. + /// This extension is absent from the [PagingController] extension for this reason. + @UseResult('Use the returned value as computed state.') + PagingState filterItems( + bool Function(ItemType item) predicate, + ) => + copyWith( + pages: pages?.map((page) => page.where(predicate).toList()).toList(), + ); } /// Helper extensions to quickly access the state of a [PagingController]. From beaf421de1476a0a199d56560e67e323a17bc2f4 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 2 Feb 2025 15:14:24 +0100 Subject: [PATCH 36/37] docs: update migration guide --- CHANGELOG.md | 7 ++++-- MIGRATION.md | 71 ++++++++++++---------------------------------------- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ea9fd3..9701299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,15 +3,18 @@ - `PagingListener` widget to connect a `PagingController` to a `PagedLayoutBuilder`. ### Changed -- `PagingController` no longer has `addPageRequestListener` method and `firstPageKey` parameter. Use the `fetchPage` and `getNextPageKey` parameters of the constructor instead. +- `PagingController` no longer has `addPageRequestListener` method and `firstPageKey` parameter. Use the `fetchPage` parameter of the constructor instead. - `PagingController` no longer has the `itemList`, `error`, and `nextPageKey` getters and setters. All values are now stored in `PagingState`. -- `PagingController` no longer has the `appendPage`, `appendLastPage`, and `retryLastFailedRequest` methods. Use the `copyWith` method of `PagingState` to update its fields. +- `PagingController` no longer has the `appendPage` and `appendLastPage` methods. Use the `copyWith` method of `PagingState` to update its `pages`, `keys`, and `hasNextPage` fields. +- `PagingController` no longer has the `retryLastFailedRequest` method. You can simply call `fetchNextPage` to try again. - `PagingController` no longer has the `invisibleItemsThreshold` field. It is now configured in `PagedChildBuilderDelegate`. +- `PagingController` now features getters matching the fields of `PagingState` as well as `mapItems` to modify the items. - `PagedLayoutBuilder` no longer accepts `pagingController` as a parameter. It now takes `PagingState` and `fetchNextPage` instead. - `PagingState` now uses `pages` (`List>`) instead of `itemList` (`List`). A new extension getter `items` is provided for flattening. - `PagingState` now features `keys`, a list storing all fetched keys, and `hasNextPage` replacing `nextPageKey`. - `PagingState` now includes `isLoading`, which tracks whether a request is in progress. - `PagingState` now provides `error` as type `Object?` instead of `dynamic`. +- `PagingState` now includes `mapItems` and `filterItems` extension methods for modifying items conveniently. ### Fixed - `PagingController` now deduplicates requests. diff --git a/MIGRATION.md b/MIGRATION.md index 7059d2b..fbd3f05 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -7,8 +7,9 @@ This is a large breaking change and will require refactoring in your code. The package was upgraded to a newer modern flutter major version. -- Newly requires `dart: ">=3.4.0"` and `flutter: ">=3.0.0"` -- Newly depends on `collection: ">=1.15.0"` +- Newly requires `dart: ">=3.4.0"` and `flutter: ">=3.0.0"` for modern language features. +- Newly depends on `collection: ">=1.15.0"` for deep collection equality on `PagingState`. +- Newly depends on `meta: ">=1.8.0"` for annotations on `PagingState` extension methods. ## PagingController @@ -44,13 +45,18 @@ This fixes several issues of the past: The PagingController can also be arbitrarily extended to include additional functionality that you might require. The source code explains how to structure new code. +Lastly, the various getter and setter methods previously featured to modify the state have been removed. +New getters have been added, however, setters have been left out since it should not be necessary to modify the state often. +One exception is the newly provided `mapItems` extension method, which can be used to modify the items in a convenient way, while retaining their page structure. + ### API Changes -- No longer features `itemList`, `error` and `nextPageKey` getters and setters. All values are directly stored in the `PagingState`. +- `itemList` and `nextPageKey` properties have been removed. +- `pages`, `items`, `keys`, `error`, `hasNextPage` and `isLoading` extension getters as well as `mapItems` to modify the items have been added. - `addPageRequestListener` was removed. Use the `fetchPage` parameter of the constructor instead. -- `appendPage` and `appendLastPage` were removed. Use the `copyWith` method of the `PagingState` to update the `pages`, `keys` and `hasNextPage` fields. +- `appendPage` and `appendLastPage` have been removed. Use the `copyWith` method of the `PagingState` to update the `pages`, `keys` and `hasNextPage` fields. - `retryLastFailedRequest` was removed. You can simply call `fetchNextPage` to try again. -- No longer accepts `invisibleItemsThreshold`. To configure the `invisibleItemsThreshold` of a layout, use the corresponding parameter of its `PagedChildBuilderDelegate`. +- `invisibleItemsThreshold` parameter has been removed. To configure the `invisibleItemsThreshold` of a layout, use the corresponding parameter of its `PagedChildBuilderDelegate`. ## PagedLayoutBuilder @@ -108,61 +114,15 @@ Examples of using a custom state management solution can be found in the example ## PagingState -The PagingState in v5 has been updated to be more flexible: +The PagingState has been updated to be more flexible: - It now includes a List of all keys, `keys`, that have been fetched, each index corresponding to a page of items. - Instead of storing the next page key, it now includes a boolean `hasNextPage` to indicate if there are more pages to fetch. - Lastly it now also includes a loading state, in `isLoading`. -Most users probably do not directly interact with PagingState beyond reading it. -However, the PagingState was also changed to allow customisation of its fields. - -Instead of storing your Query parameters externally, you can now extend the PagingState to include them: - -```dart -final class MyPagingState - extends PagingStateBase { - MyPagingState({ - super.pages, - // ... - this.query, - }); - - final String? query; - - @override - PagingState copyWith({ - Defaulted>?>? pages = const Omit(), - // ... - Defaulted? query = const Omit(), - }) { - final partial = super.copyWith( - pages: pages, - // ... - isLoading: isLoading, - ); - - return MyPagingState( - pages: partial.pages, - // ... - query: query is Omit ? this.query : query as String?, - ); - } - - @override - PagingState reset() { - final partial = super.reset(); - - return MyPagingState( - pages: partial.pages, - // ... - query: null, - ); - } -} -``` - -Extended PagingStates will correctly work with PagingController, thanks to the `copyWith` and `reset` methods. For a better understanding of how this works, check out the source code. +Because Items are now stored within pages, it is more difficult to modify the items directly. +To make this easier, a `mapItems` extension method has been added to modify the items by iterating over them. +Additionally, a `filterItems` extension method has been added to filter the items. This is useful for creating locally filtered computed states. ### API Changes @@ -171,3 +131,4 @@ Extended PagingStates will correctly work with PagingController, thanks to the ` - `error` is now type Object? instead of dynamic. - `nextPageKey` was removed. You can use the `keys` field to compute the next page and `hasNextPage` to determine if there are more pages. - `isLoading` is a new field that indicates if a request is currently in progress. +- `mapItems` and `filterItems` have been added to modify the items in a convenient way. From e5a54d15ccb62d55c2ea2017bb25364fc63b2249 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 2 Feb 2025 15:26:02 +0100 Subject: [PATCH 37/37] docs: update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9701299..140034b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ ### Fixed - `PagingController` now deduplicates requests. - `PagingController` refresh operations now cancel previous requests. +- Off-by-one error in `invisibleItemsThreshold` calculation. +- Failure to trigger page request when `invisibleItemsThreshold` is too large. +- Animating between states with `animateTransitions`. ## 4.1.0 - 2024-11-09 ### Added