riverpod_infinite_scroll 1.0.11 copy "riverpod_infinite_scroll: ^1.0.11" to clipboard
riverpod_infinite_scroll: ^1.0.11 copied to clipboard

Riverpod implementation of infinite_scroll_pagination, this package allows to use infinite_scroll_pagination with Riverpod abstracting away the complexity

Riverpod Infinite Scroll #

Hi! This package is a plugin for infinite_scroll_pagination that is designed to work with Riverpod.

Easy Custom
easy custom

Getting started: #

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';
copied to clipboard

How it works #

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 extends PagedNotifier a notifier that has all the properties that riverpod_infinite_scroll needs and is intended for simple states only containing a list of records

  • Or if you need more flexbility to handle a more complex state object you can use a StateNotifier that uses PagedState (or a state that extends PagedState) - in such case a mixin that ensures your StateNotifier will implement the load method with the correct types is provided with PagedNotifierMixin

Example - Simple version #

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),
          ),
        );
      }
    }
copied to clipboard

As we can see RiverPagedBuilder is small and easy to implement with the following properties:

  1. firstPageKey - the first page we sent to our paginated API
  2. provider - The StateNotifierProvider that holds the logic and the list of Posts
  3. itemBuilder - a function that builds a single Post
  4. pagedBuilder - The type of list we want to render. This can be any of the infinite_scroll_pagination widgets, and this package already gives us the PaginationController and the BuilderDelegate

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 });
    }
copied to clipboard

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());
copied to clipboard

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);
    };
copied to clipboard

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 ]);
}
copied to clipboard

A more custom example #

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,
              ),
            ),
          ),
        );
      }
    }
copied to clipboard

We have used a PagedGridView here instead of a PagedListView only to make things more fun. This package works with any of the infinite_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 });
    }
copied to clipboard

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());

copied to clipboard

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 {
copied to clipboard

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',
copied to clipboard

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());
copied to clipboard

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);
   		    );
   	    }
    }
copied to clipboard

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> {
copied to clipboard

You could for example pass an Enum:

class CustomExampleNotifier extends StateNotifier<CustomExampleState>
    with PagedNotifierMixin<MyEnumType, User, CustomExampleState> {
copied to clipboard

and then just change the Generics of load and RiverPagedBuilder and your state object that extends PagedState to match.

Custom wrapper for loading/error/try again states #

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 call
  • newPageProgressIndicatorBuilder - a builder for the loading state for the subsequent requests
  • firstPageErrorIndicatorBuilder - a builder for the error state in the first call
  • newPageErrorIndicatorBuilder - a builder for the error state for the subsequent requests
  • noItemsFoundIndicatorBuilder - a builder for the empty state in the first call
  • noMoreItemsIndicatorBuilder - 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!

Testing #

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

62
likes
130
points
1.17k
downloads

Publisher

verified publisherquantos.it

Weekly Downloads

2024.09.20 - 2025.04.04

Riverpod implementation of infinite_scroll_pagination, this package allows to use infinite_scroll_pagination with Riverpod abstracting away the complexity

Homepage
Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, flutter_riverpod, infinite_scroll_pagination

More

Packages that depend on riverpod_infinite_scroll