Hi! This package is a plugin for infinite_scroll_pagination that is designed to work with Riverpod.
Easy | Custom |
---|---|
flutter pub get riverpod_infinite_scroll
flutter pub get infinite_scroll_pagination
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:riverpod_infinite_scroll/riverpod_infinite_scroll.dart';
This package exports a widget, RiverPagedBuilder
which builds your infinite, scrollable list.
RiverPagedBuilder
expects a Riverpod StateNotifierProvider
This StateNotifierProvider
must implement two methods to ensure everything works correctly, it must have a load method
, and a nextPageKeyBuilder
method, these will be explained below.
riverpod_infinite_scroll
ensures our StateNotifier
will respect these constraints with the choice of two classes:
You can either use the simple:
-
PagedNotifier
- You can create a class that extendsPagedNotifier
a notifier that has all the properties thatriverpod_infinite_scroll
needs and is intended for simple states only containing a list ofrecords
-
Or if you need more flexbility to handle a more complex state object you can use a
StateNotifier
that usesPagedState
(or a state that extends PagedState) - in such case a mixin that ensures yourStateNotifier
will implement theload
method with the correct types is provided withPagedNotifierMixin
Let's see an example now! We have an API that returns a list of Post
objects, this API is paginated and we need to show a feed displaying those Posts.
The widget we will use for displaying such a feed is RiverPagedBuilder!
. Refer to source code: easy_example.dart
class EasyExample extends StatelessWidget {
const EasyExample({Key? key} :super(key: key);
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(),
body: RiverPagedBuilder<int, Post>(
firstPageKey: 0,
provider: easyExampleProvider,
itemBuilder: (context, item, index) => ListTile(
leading: Image.network(item.image),
title: Text(item.title),
),
pagedBuilder: (controller, builder) =>
PagedListView(pagingController: controller, builderDelegate: builder),
),
);
}
}
As we can see RiverPagedBuilder
is small and easy to implement with the following properties:
firstPageKey
- the first page we sent to our paginated APIprovider
- TheStateNotifierProvider
that holds the logic and the list of PostsitemBuilder
- a function that builds a single PostpagedBuilder
- The type of list we want to render. This can be any of theinfinite_scroll_pagination
widgets, and this package already gives us thePaginationController
and theBuilderDelegate
Let's see how our StateNotifier
works.
Here is our model Post
:
class Post {
final int id;
final String title;
final String image;
const Post({ required this.id, required this.title, required this.image });
}
And the StateNotifier
. Source code: easy_example_provider.dart
class EasyExampleNotifier extends PagedNotifier<int, Post> {
EasyExampleNotifier():
super(
//load is a required method of PagedNotifier
load: (page, limit) => Future.delayed(const Duration(seconds: 2), () {
// This simulates a network call to an api that returns paginated posts
return [
const Post(id: 1, title: "My first work", image: "https://www.mywebsite.com/image1"),
const Post(id: 2, title: "My second work", image: "https://www.mywebsite.com/image2"),
const Post(id: 3, title: "My third work", image: "https://www.mywebsite.com/image3"),
];
}),
//nextPageKeyBuilder is a required method of PagedNotifier
nextPageKeyBuilder: NextPageKeyBuilderDefault.mysqlPagination,
);
// Example of custom methods you are free to implement in StateNotifier
void add(Post post) {
state = state.copyWith(records: [ ...(state.records ?? []), post ]);
}
void delete(Post post) {
state = state.copyWith(records: [ ...(state.records ?? []) ]..remove(post));
}
}
//create a global provider as you would normally in riverpod:
final easyExampleProvider = StateNotifierProvider<EasyExampleNotifier, PagedState<int, Post>>((_) => EasyExampleNotifier());
We can extend PagedNotifier
which is a child of StateNotifier
and everything will be done for us.
PagedNotifier
only asks for a load function, and a nextPageKeyBuilder
function that returns the next page. and that's it!
In the example above we used NextPageKeyBuilderDefault.mysqlPagination
, a default function to reduce boilerplate.
NextPageKeyBuilder<int, dynamic> mysqlPagination =
(List<dynamic>? lastItems, int page, int limit) {
return (lastItems == null || lastItems.length < limit) ? null : (page + 1);
};
Also notice the records
member of the internal state
object of PagedNotifier
is accessible and modifiable in the standard Riverpod way through this custom function add
void add(Post post) {
state = state.copyWith(records: [ ...(state.records ?? []), post ]);
}
If you need to keep track of a more complex state than a simple list of records
Riverpod Infinite Scroll also provides a more customizable approach.
Let's suppose we need to fetch from a paginated API that return a list of users. Source code: (custom_example.dart)[https://github.com/ftognetto/riverpod_infinite_scroll/blob/main/example/lib/custom/custom_example.dart]
class CustomExample extends StatelessWidget {
const CustomExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: RiverPagedBuilder<String, User>(
firstPageKey: 'FirstPage',
provider: customExampleProvider,
itemBuilder: (context, item, index) => ListTile(
leading: Image.network(item.profilePicture),
title: Text(item.name),
),
pagedBuilder: (controller, builder) => PagedGridView(
pagingController: controller,
builderDelegate: builder,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
),
),
);
}
}
We have used a
PagedGridView
here instead of aPagedListView
only to make things more fun. This package works with any of theinfinite_scroll_pagination
widgets.
Now let's have a look of how we can create a more custom StateNotifier
, first a simple class to represent a User:
class User {
final String id;
final String name;
final String profilePicture;
const User({ required this.id, required this.name, required this.profilePicture });
}
And we have the StateNotifier
that manages those users. Source code: (custom_example_provider.dart)[https://github.com/ftognetto/riverpod_infinite_scroll/blob/main/example/lib/custom/custom_example_provider.dart]
class CustomExampleNotifier extends StateNotifier<CustomExampleState>
with PagedNotifierMixin<String, User, CustomExampleState> {
CustomExampleNotifier() : super(const CustomExampleState());
@override
Future<List<User>?> load(String page, int limit) async {
try {
//as build can be called many times, ensure
//we only hit our page API once per page
if (state.previousPageKeys.contains(page)) {
await Future.delayed(const Duration(seconds: 0), () {
state = state.copyWith();
});
return state.records;
}
var users = await Future.delayed(const Duration(seconds: 3), () {
// This simulates a network call to an api that returns paginated users
return [
const User(
id: "abcdef",
name: "John",
profilePicture: "https://www.mywebsite.com/images/1"),
const User(
id: "asdfgh",
name: "Mary",
profilePicture: "https://www.mywebsite.com/images/2"),
const User(
id: "qwerty",
name: "Robert",
profilePicture: "https://www.mywebsite.com/images/3")
];
});
// we then update state accordingly
state = state.copyWith(records: [
...(state.records ?? []),
...users
], nextPageKey: users.length < limit ? null : users[users.length - 1].id,
previousPageKeys: {...state.previousPageKeys, page}.toList());
} catch (e) {
// in case of error we should notifiy the listeners
state = state.copyWith(error: e.toString());
}
}
// Super simple example of custom methods of the StateNotifier
void add(User user) {
state = state.copyWith(records: [...(state.records ?? []), user]);
}
void delete(User user) {
state = state.copyWith(records: [...(state.records ?? [])]..remove(user));
}
}
final customExampleProvider =
StateNotifierProvider<CustomExampleNotifier, CustomExampleState>(
(_) => CustomExampleNotifier());
We didn't use PagedNotifier
, instead we used a normal Riverpod StateNotifier
with the PagedNotifierMixin
which ensures the notifier has a correctly typed load
method.
Let's take a closer look at :
Future<List<User>?> load(String page, int limit) async {
Where does this String page
get set? Well some of you may have noticed this firstPageKey
whatever string is in there will be passed to the page
argument of load
:
body: RiverPagedBuilder<String, User>(
firstPageKey: 'FirstPage',
It is also important to note that you are responsible for maintaining the records
list:
state = state.copyWith(records: [
...(state.records ?? []),
...users
], nextPageKey: users.length < limit ? null : users[users.length - 1].id,
previousPageKeys: {...state.previousPageKeys, page}.toList());
Also, in this example, we have used a custom state that extends PagedState
, because we need another custom parameter filterByCity
:
class CustomExampleState extends PagedState<String, User> {
// We can extends [PagedState] to add custom parameters to our state
final bool filterByCity;
const CustomExampleState({
this.filterByCity = false,
List<User>? records,
String? error,
String? nextPageKey,
List<String>? previousPageKeys }):
super(records: records, error: error, nextPageKey: nextPageKey);
// We can customize our .copyWith for example
@override
CustomExampleState copyWith({
bool? filterByCity,
List<User>? records,
dynamic error,
dynamic nextPageKey,
List<String>? previousPageKeys
}){
final sup = super.copyWith(
records: records,
error: error,
nextPageKey: nextPageKey,
previousPageKeys: sup.previousPageKeys);
);
return CustomExampleState(
filterByCity: filterByCity ?? this.filterByCity,
records: sup.records,
error: sup.error,
nextPageKey: sup.nextPageKey,
previousPageKeys: sup.previousPageKeys);
);
}
}
Your custom arg for firstPageKey
does not have to be a String
it can be any type as specified when you declared your Notifier:
class CustomExampleNotifier extends StateNotifier<CustomExampleState>
with PagedNotifierMixin<String, User, CustomExampleState> {
You could for example pass an Enum:
class CustomExampleNotifier extends StateNotifier<CustomExampleState>
with PagedNotifierMixin<MyEnumType, User, CustomExampleState> {
and then just change the Generics of load
and RiverPagedBuilder
and your state object that extends PagedState
to match.
The RiverPagedBuilder
offers, other than the properties we already saw, the same properties that infinite_scroll_pagination
offers.
firstPageProgressIndicatorBuilder
- a builder for the loading state in the first callnewPageProgressIndicatorBuilder
- a builder for the loading state for the subsequent requestsfirstPageErrorIndicatorBuilder
- a builder for the error state in the first callnewPageErrorIndicatorBuilder
- a builder for the error state for the subsequent requestsnoItemsFoundIndicatorBuilder
- a builder for the empty state in the first callnoMoreItemsIndicatorBuilder
- a builder for the empty state for the subsequent request (we have fetched all the items!)
If we need to give a coherent design to our app we could wrap the RiverPagedBuilder
into a new Widget!
An integration test is provided demonstrating how easy it is to test this widget: https://github.com/ftognetto/riverpod_infinite_scroll/blob/main/example/integration_test/app_test.dart