infinite_scroll_pagination 2.0.0 infinite_scroll_pagination: ^2.0.0 copied to clipboard
Load and display pages of items as the user scrolls down your screen.
Cookbook #
All the snippets below were extracted from the example project.
Simple Usage #
class CharacterListView extends StatefulWidget {
@override
_CharacterListViewState createState() => _CharacterListViewState();
}
class _CharacterListViewState extends State<CharacterListView> {
static const _pageSize = 20;
final PagingController<int, CharacterSummary> _pagingController =
PagingController(firstPageKey: 0);
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
void _fetchPage(int pageKey) {
RemoteApi.getCharacterList(pageKey, _pageSize).then((newItems) {
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
}).catchError((error) {
_pagingController.error = error;
});
}
@override
Widget build(BuildContext context) => PagedListView<int, CharacterSummary>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
);
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}
Separators #
@override
Widget build(BuildContext context) =>
PagedListView<int, CharacterSummary>.separated(
pagingController: _pagingController,
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 or 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 comes with 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: (searchTerm) => _updateSearchTerm(searchTerm),
),
PagedSliverList<int, CharacterSummary>(
pagingController: _pagingController,
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 a function named _updateSearchTerm
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's the complete code:
class CharacterSliverList extends StatefulWidget {
@override
_CharacterSliverListState createState() => _CharacterSliverListState();
}
class _CharacterSliverListState extends State<CharacterSliverList> {
static const _pageSize = 17;
final PagingController _pagingController =
PagingController<int, CharacterSummary>(firstPageKey: 0);
Object _activeCallbackIdentity;
String _searchTerm;
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
void _fetchPage(pageKey) {
final callbackIdentity = Object();
_activeCallbackIdentity = callbackIdentity;
RemoteApi.getCharacterList(pageKey, _pageSize, searchTerm: _searchTerm)
.then((newItems) {
if (callbackIdentity == _activeCallbackIdentity) {
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
}
}).catchError((error) {
if (callbackIdentity == _activeCallbackIdentity) {
_pagingController.error = error;
}
});
}
@override
Widget build(BuildContext context) => CustomScrollView(
slivers: <Widget>[
CharacterSearchInputSliver(
onChanged: _updateSearchTerm,
),
PagedSliverList<int, CharacterSummary>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
),
],
);
void _updateSearchTerm(String searchTerm) {
_searchTerm = searchTerm;
_pagingController.refresh();
}
@override
void dispose() {
_activeCallbackIdentity = null;
_pagingController.dispose();
super.dispose();
}
}
The same structure can be applied to all kinds of filtering and sorting and works with any layout (not just Slivers).
Pull-to-Refresh #
Simply wrap your PagedListView, PagedGridView or CustomScrollView with a RefreshIndicator (from the material library) and inside onRefresh, call refresh
on your PagingController instance:
@override
Widget build(BuildContext context) => RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, CharacterSummary>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<CharacterSummary>(
itemBuilder: (context, item, index) => CharacterListItem(
character: item,
),
),
),
);
Listening to Status Changes #
If you need to execute some 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. For example:
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
_pagingController.addStatusListener((status) {
if (status == PagingStatus.subsequentPageError) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: const Text(
'Something went wrong while fetching a new page.',
),
action: SnackBarAction(
label: 'Retry',
onPressed: () => _pagingController.retryLastRequest(),
),
),
);
}
});
super.initState();
}
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>(
pagingController: pagingController,
builderDelegate: builderDelegate,
completedListingBuilder: (
context,
itemBuilder,
itemCount,
) =>
SliverGrid(
gridDelegate: gridDelegate,
delegate: _buildSliverDelegate(
itemBuilder,
itemCount,
),
),
loadingListingBuilder: (
context,
itemBuilder,
itemCount,
progressIndicatorBuilder,
) =>
SliverGrid(
gridDelegate: gridDelegate,
delegate: _buildSliverDelegate(
itemBuilder,
itemCount,
statusIndicatorBuilder: progressIndicatorBuilder,
),
),
errorListingBuilder: (
context,
itemBuilder,
itemCount,
errorIndicatorBuilder,
) =>
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();
final PagingController<int, CharacterSummary> _pagingController =
PagingController(firstPageKey: 0);
StreamSubscription _blocListingStateSubscription;
@override
void 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,
);
});
super.initState();
}
@override
Widget build(BuildContext context) => CustomScrollView(
slivers: <Widget>[
CharacterSearchInputSliver(
onChanged: (searchTerm) => _bloc.onSearchInputChangedSink.add(searchTerm),
),
PagedSliverGrid<int, CharacterSummary>(
pagingController: _pagingController,
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() {
_pagingController.dispose();
_blocListingStateSubscription.cancel();
super.dispose();
}
}
Check out the example project for the complete source code.