infinite_scroll_pagination 1.0.0+2 infinite_scroll_pagination: ^1.0.0+2 copied to clipboard
Load and display pages of items as the user scrolls down your screen.
Cookbook #
All snippets below were extracted from the example project.
Simple Usage #
class CharacterListView extends StatefulWidget {
@override
_CharacterListViewState createState() => _CharacterListViewState();
}
class _CharacterListViewState extends State<CharacterListView> {
final CharacterListViewDataSource _dataSource = CharacterListViewDataSource();
@override
Widget build(BuildContext context) =>
PagedListView<int, CharacterSummary>(
dataSource: _dataSource,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
);
@override
void dispose() {
_dataSource.dispose();
super.dispose();
}
}
class CharacterListViewDataSource
extends PagedDataSource<int, CharacterSummary> {
CharacterListViewDataSource() : super(0);
static const _pageSize = 20;
@override
void fetchItems(int pageKey) {
RemoteApi.getCharacterList(pageKey, _pageSize).then((newItems) {
final hasFinished = newItems.length < _pageSize;
final nextPageKey = hasFinished ? null : pageKey + newItems.length;
notifyNewPage(newItems, nextPageKey);
}).catchError(notifyError);
}
}
Separators #
@override
Widget build(BuildContext context) => PagedListView<int, CharacterSummary>.separated(
dataSource: _dataSource,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
separatorBuilder: (context, index) => const Divider(),
);
Works for both PagedListView and PagedSliverList.
Preceding/Following Items #
If you need to add preceding/following widgets that are expected to scroll along with your list, such as a header or a footer, you should use our Sliver widgets. Infinite Scroll Pagination gives you PagedSliverList and PagedSliverGrid, which works almost the same as PagedListView or PagedGridView, except that they need to be wrapped by a CustomScrollView. That allows you to give them siblings, for example:
@override
Widget build(BuildContext context) =>
CustomScrollView(
slivers: <Widget>[
CharacterSearchInputSliver(
onChanged: _dataSource.updateSearchTerm,
),
PagedSliverList<int, CharacterSummary>(
dataSource: _dataSource,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
),
],
);
Notice that your preceding/following widgets should also be Slivers. CharacterSearchInputSliver
, for example, is nothing but a TextField wrapped by a SliverToBoxAdapter.
If you're adding a single widget, as in the example, SliverToBoxAdapter will do the job. If you need to add a list of preceding/following items, you can use a SliverList.
Searching/Filtering/Sorting #
In the preceding recipe, you can see how to add a search bar widget as a list header. That example calls updateSearchTerm
in the PagedDataSource subclass every time the user changes the search input. That function isn't part of the package, it's just a suggestion on how to implement searching. Here you can see how that function is implemented inside the sample's PagedDataSource subclass:
class CharacterSliverListDataSource
extends PagedDataSource<int, CharacterSummary> {
CharacterSliverListDataSource() : super(0);
static const _pageSize = 17;
// Simple mechanism to avoid having overlapping HTTP requests.
Object _activeCallbackIdentity;
String _searchTerm;
@override
void fetchItems(int pageKey) {
final callbackIdentity = Object();
_activeCallbackIdentity = callbackIdentity;
RemoteApi.getCharacterList(pageKey, _pageSize, searchTerm: _searchTerm)
.then((newItems) {
if (callbackIdentity == _activeCallbackIdentity) {
final hasFinished = newItems.length < _pageSize;
notifyNewPage(newItems, hasFinished ? null : pageKey + newItems.length);
}
}).catchError((error) {
if (callbackIdentity == _activeCallbackIdentity) {
notifyError(error);
}
});
}
void updateSearchTerm(String searchTerm) {
_searchTerm = searchTerm;
refresh();
}
@override
void dispose() {
_activeCallbackIdentity = null;
super.dispose();
}
}
The same structure can be applied to all kinds of filtering and sorting.
Pull-to-Refresh #
Just wrap your PagedListView, PagedGridView or CustomScrollView with a RefreshIndicator (from the material library) and inside onRefresh, call refresh
on your PagedDataSource instance:
@override
Widget build(BuildContext context) =>
RefreshIndicator(
onRefresh: () => Future.sync(
_dataSource.refresh,
),
child: PagedListView<int, CharacterSummary>(
dataSource: _dataSource,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
),
);
Custom Layout #
In case PagedListView, PagedSliverList, PagedGridView and PagedSliverGrid doesn't work for you, you should create a new sliver layout.
Creating a new layout is just a matter of using PagedSliverBuilder 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 is built:
@override
Widget build(BuildContext context) =>
PagedSliverBuilder<PageKeyType, ItemType>(
dataSource: dataSource,
builderDelegate: builderDelegate,
invisibleItemsThreshold: invisibleItemsThreshold,
shouldRetryOnScroll: shouldRetryOnScroll,
completedListingBuilder: (
context,
itemBuilder,
itemCount,
) =>
SliverGrid(
gridDelegate: gridDelegate,
delegate: _buildSliverDelegate(
itemBuilder,
itemCount,
),
),
loadingListingBuilder: (
context,
itemBuilder,
itemCount,
progressIndicatorBuilder,
) =>
// SliverGrid with a progress indicator as its last item.
SliverGrid(
gridDelegate: gridDelegate,
delegate: _buildSliverDelegate(
itemBuilder,
itemCount,
statusIndicatorBuilder: progressIndicatorBuilder,
),
),
errorListingBuilder: (
context,
itemBuilder,
itemCount,
errorIndicatorBuilder,
) =>
// SliverGrid with an error indicator as its last item.
SliverGrid(
gridDelegate: gridDelegate,
delegate: _buildSliverDelegate(
itemBuilder,
itemCount,
statusIndicatorBuilder: errorIndicatorBuilder,
),
),
);
Notice that your resulting widget will be a Sliver, and as such, you need to wrap it with a CustomScrollView before adding to the screen.
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, it's just one of the possible ways to integrate it with BLoCs:
class _CharacterSliverGridState extends State<CharacterSliverGrid> {
final CharacterSliverGridBloc _bloc = CharacterSliverGridBloc();
CharacterSliverGridDataSource _dataSource;
StreamSubscription _blocListingStateSubscription;
@override
void initState() {
_dataSource = CharacterSliverGridDataSource(
onPageRequested: _bloc.onPageRequestSink.add,
);
// We could have used StreamBuilder, but that would unnecessarily recreate
// the entire [PagedSliverGrid] every time the state changes.
// Instead, handling the subscription ourselves and updating only the
// _dataSource is more efficient.
_blocListingStateSubscription =
_bloc.onNewListingState.listen((listingState) {
_dataSource.notifyChange(
listingState.itemList,
listingState.error,
listingState.nextKey,
);
});
super.initState();
}
@override
Widget build(BuildContext context) => CustomScrollView(
slivers: <Widget>[
CharacterSearchInputSliver(
onChanged: _bloc.onSearchInputChangedSink.add,
),
PagedSliverGrid<int, CharacterSummary>(
dataSource: _dataSource,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 100 / 150,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 3,
),
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterGridItem(
character: item,
),
),
),
],
);
@override
void dispose() {
_dataSource.dispose();
_blocListingStateSubscription.cancel();
super.dispose();
}
}
class CharacterSliverGridDataSource
extends PagedDataSource<int, CharacterSummary> {
CharacterSliverGridDataSource({
@required this.onPageRequested,
}) : super(0);
final ValueChanged<int> onPageRequested;
@override
void fetchItems(int pageKey) => onPageRequested(pageKey);
}
Check out the example project for the complete source code.