The easiest infinite scrolling pagination using Riverpod. Just initialize your AsyncNotifier build method with your data-fetching repository method - no need to write any other logic.
Checkout video: https://github.com/hafees/riverpod_infinite_scroll/assets/925404/128aadef-c14e-408b-a042-a3d42b67cc85
PaginatedListView(
state: ref.watch(searchMoviesProvider),
itemBuilder: (data) => MovieItem(movie: data),
notifier: ref.read(searchMoviesProvider.notifier),
),
Simple code like this can produce, infinite scroll pagination (The user scrolls to the end of the list and the next set of data is loaded automatically).
Features
- Easiest implementation of infinite scrolling pagination ever
- Supports ListView, ListView.separator, SliverList, SliverList.se and GridView and SliverGrids
- Skeleton loading animation support
- Default Widgets for initial loading, inline loading, and error
- Custom builders allow you to customize all behaviors of the package
- A data fetcher class that you may use independently to store paginated data
- Well documented and an example app is provided for reference
Getting started
You will need Riverpod to use this package. If you're not using it, is an excellent state management library. See Riverpod documentation. Also start using, Riverpod generators.
You can use this package on a Riverpod-generated AsyncNotifier. Also, there are two widgets - PaginatedListView for ListView builds and PaginatedGridView for GridView builds.
Usage
As usual in the normal Riverpod state management application, there is a provider (for managing state), repository(for fetching data), and widgets (for UI interface). If you're not familiar with the Riverpod package, then see the documentation: https://riverpod.dev/docs/introduction/why_riverpod.
In your provider:
You will need to use the PaginatedDataMixin mixin and should implement the PaginatedNotifier class.
Example:
//A normal riverpod notifier.
@riverpod
class TrendingMoviesList extends _$TrendingMoviesList
with PaginatedDataMixin<TmdbMovie> // The mixin you should use
implements PaginatedNotifier<TmdbMovie> {
//As usual, you should override the build method
@override
FutureOr<List<TmdbMovie>> build() async {
return init(
dataFetcher: PaginatedDataRepository(
fetcher: ref.watch(tmdbRepositoryProvider).getTrendingMovies,
), //Initialise with your data fetching method
);
}
}
You may have noticed that there is an init
method in the build() function. This is where we initialize our pagination and data fetching. The PaginatedDataRepository class is used for storing paginated data. You should initialize it with a fetcher method. This can be your repository method for retrieving paginated data. Otherwise, you can define the fetching function directly (Recommended as you can avoid creating repositories for simple data fetching).
The fetcher
method will receive two parameters. The page
to fetch and query
that you can use to filter the data. You only need to provide data according to these parameters. The pagination logic, scroll handlers, state changes etc are automatically handled by the package.
Note: For notifiers with keepAlive:true
, you will need to use the KeepAlivePaginatedDataMixin. Hopefully, this limitation can be removed in future versions.
Your repository
This is a sample code for the repository.
Future<PaginatedResponse<TmdbMovie>> searchMovies({
int page = 1,
String? query = '',
}) async {
await Future<void>.delayed(const Duration(seconds: 2));
final results = await dio.get<Map<String, dynamic>>(
'search/movie?query=$query&include_adult=false&page=$page',
);
return PaginatedResponse.fromJson(
results.data!,
dataMapper: TmdbMovie.fromJson,
dataField: 'results',
paginationParser: (data) => Pagination(
totalNumber: data['total_results'] as int,
currentPage: data['page'] as int,
lastPage: data['total_pages'] as int,
),
);
}
Your repository method should accept a page
and query
params. You can use these parameters in your data-fetching logic.
-
The
dataMapper
function is a fromJson method that can be used to convert JSON data to models. If you usefreezed
package for generating models, this is created automatically. -
The
dataField
is to identify the data part from the JSON data. The TMDB API returns the paginated movie data in 'results' field and hence we're usingdataFied:'results'
. -
The
paginationParser
field is a callback function that will receive the whole JSON data and you can parse data and return a Pagination object. The above code is suitable for TMDB API.
If you're using the Laravel framework, then the JSON structure may look like the following.
{
"total": 50,
"per_page": 15,
"current_page": 1,
"last_page": 4,
"first_page_url": "http://laravel.app?page=1",
"last_page_url": "http://laravel.app?page=4",
"next_page_url": "http://laravel.app?page=2",
"prev_page_url": null,
"path": "http://laravel.app",
"from": 1,
"to": 15,
"data": [
{
// Record...
},
{
// Record...
}
]
}
So, to parse data and pagination we need something like,
return PaginatedResponse.fromJson(
results.data!,
dataMapper: TmdbMovie.fromJson,
dataField: 'data', //Since this is the default dataField, you can omit this
paginationParser: (data) => Pagination(
totalNumber: data['total'] as int,
currentPage: data['current_page'] as int,
lastPage: data['last_page'] as int,
),
);
Your widget tree
There are two widgets. PaginatedListView
and PaginatedGridView
.
PaginatedListView Builds a list using the Flutter ListView widget.
Example
PaginatedListView(
state: ref.watch(searchMoviesProvider.notifier),
itemBuilder: (data) => MovieItem(movie: data),
notifier: ref.read(searchMoviesProvider.notifier),
),
PaginatedGridView Builds a list using the Flutter GridView widget.
Example
PaginatedGridView(
state: ref.watch(searchMoviesProvider),,
itemBuilder: (data) => MovieGridItem(movie: data),
notifier: ref.read(searchMoviesProvider.notifier),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1 / 1.22,
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
);
Customisation
You can pass a skeleton
to create skeleton loading animation.
Example
PaginatedListView(
state: ref.watch(searchMoviesProvider),
itemBuilder: (data) => MovieItem(movie: data),
notifier: ref.read(searchMoviesProvider.notifier),
skeleton: MovieItem(
movie: TmdbMovie(
originalTitle: 'Dummy Title',
overview:'Long text summary \n Another line of text',
),
),
numSkeletons: 8, // The number of skeletons to show
),
It uses the Skeletonizer dart package for building skeleton animation. If the default animations need to be customised you can include a SkeletonizerConfig
widget in root level or as a parent widget.
Example
SkeletonizerConfig(
data: const SkeletonizerConfigData(
effect: PulseEffect(from: Colors.white10, to: Colors.white24),
),
child: <Your child widget tree>
),
You can also use builder methods for customising the output. The following builders are available.
initialLoadingBuilder
: To customise the initial loading.
loadingBuilder
: To customise the loading animation when next page is fetched.
emptyListBuilder
: What to show when the fetched data is empty
errorBuilder
: When there is an error
Using Slivers
You just need to set the useSliver
parameter to true
to get sliver widgets.
Note: When useSliver
is true, you will need to create a ScrollController and attach it to the CustomScrollView and then pass the scrollController
.
Example
class MovieListSliver extends ConsumerWidget {
MovieListSliver({super.key});
final scrollController = ScrollController(); //Create a scroll controller
@override
Widget build(BuildContext context, WidgetRef ref) {
final movies = ref.watch(trendingMoviesListProvider);
return Scaffold(
body: CustomScrollView(
controller: scrollController, //Attach it to the CustomScrollView
slivers: [
PaginatedListView(
state: movies,
itemBuilder: (data) => MovieItem(movie: data),
notifier: ref.read(trendingMoviesListProvider.notifier),
useSliver: true,
scrollController: scrollController, // Pass the scroll controller
),
],
),
);
}
}
Additional information
You can check out the package and check the example folder for a movie listing app that fetches data using TMDB API. I have provided examples for ListView, SliverLists, GridView, query parameters, implementing pull-to-refresh functionality with CustomScrollView, etc.
Limitations
I have tested the package with Riverpod AsyncNotifiers - both keepAlive and autodisposed. However, if you need to accept parameters in your build method, you should do some workarounds. Hopefully, this can be fixed in future versions (Might need to get some information from the Riverpod author).
For the time being, you can either alter your logic to use the query filter instead of accepting parameters in build()
or you can use another mixin - PaginatedDataMixinGeneric in your provider and override the following methods. (Just and copy and paste this methods and it should work fine)
@override
Future<void> getNextPage() async {
state = const AsyncLoading();
state = AsyncData(await fetchData());
}
@override
Future<void> refresh() async {
state = const AsyncLoading();
state = AsyncData(await reloadData());
}
More Examples
Checkout more code samples: https://pub.dev/packages/riverpod_infinite_scroll_pagination/example
Also you can checkout the example project.
Libraries
- riverpod_infinite_scroll_pagination
- Riverpod Infinite Scroll Library.