Riverpod and Hooks: Unlocking the Power of Pagination

This blog covers how to use Hooks, a widget state management method, and Riverpod, a Flutter state management library, to develop effective pagination in your Flutter applications. With the ability to present enormous quantities of data sequentially and enhance user experience, pagination is an essential feature in many apps. In this tutorial, we’ll demonstrate how to build up pagination using Riverpod and Hooks, allowing you to design dynamic, adaptable user interfaces for your Flutter projects.

Pagination

A fundamental concept in frontend programming is pagination, which enables us to load a finite amount of data simultaneously—both the user and the server benefit from this. By saving the user from having to wait for all of the material to load at once, it enhances their experience. By delivering only the necessary data, it reduces the burden on the server.

Pagination Types

Pagination is not a universally applicable solution. It is available in various flavors, each with a unique taste. A preview of the pagination kinds we’ll be analyzing in the following sections is provided below:

1. Page/Offset Pagination:

This traditional pagination method relies on the server’s ability to determine data limitations and offsets. The frontend only specifies the desired page number or offset; the back end performs processing-intensive tasks.

2. Cursor/Keyset/Seek/Time-Based Pagination:

This pagination necessitates more communication between the front and back-end development. We inform the server of the most recent itemItem the user has viewed. It might be a field critical to your dataset, such as an item ID (in Cursor pagination) or a timestamp (in Time-Based pagination).

Using Flutter

In recent frontend development, there are two primary types of pagination: one where data from the most recent itemItem viewed is sent to the back-end and the other where page or offset details are calculated and sent. Since the back end controls the query and data structure, it typically decides which method to utilize.

1. Page/Offset Pagination:

When page/offset pagination is used, the request structure frequently resembles the JSON object in Flutter shown below:

Example

{ "page": 1,
    "perpage": 10,
    "search": "...other filter"
}

2. Paging using the cursor:

This pagination introduces a new request structure:

Example

{
   "cursor": "Item-12",
   "search":....// other filter
}

A cursor that usually points to the last item obtained in a previous request serves as the focal point of this request. Additionally, search filters could be present. In response, the back-end returns JSON objects such as:

Code:

{  
  "data": [
    {"id": "Item-13", "name": "Item 13"},
    {"id": "Item-14", "name": "Item 14"},
    {"id": "Item-15", "name": "Item 15"},
    {"id": "Item-16", "name": "Item 16"},
    {"id": "Item-17", "name": "Item 17"},
    {"id": "Item-18", "name": "Item 18"},
    {"id": "Item-19", "name": "Item 19"},
    {"id": "Item-20", "name": "Item 20"},
    {"id": "Item-21", "name": "Item 21"},
    {"id": "Item-22", "name": "Item 22"}
  ],
  "hasMore": true // some api may not return this and some may return it
}

Run the following command to add the required package before starting the code.

dart pub add dev:json_serializable json_annotation dev:build_runner flutter_hooks hooks_riverpod flutter_riverpod riverpod

Transform to Model

Using the toJson/fromJson function to transform the JSON response into Dart models to implement pagination in your Dart application efficiently is crucial. Here’s how to use the json_serializable library to define Dart models.

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<T> {
  final MetaData meta;
  final List<T> data;


  PaginationResponse({required this.meta, required this.data});


  factory PaginationResponse.fromJson(
          Map<String, dynamic> json) =>
      _$PaginationResponseFromJson(json);


Map<String, dynamic> toJson = >   _$PaginationResponseFromJson(this);
}

Pagination Logic Isolation for Reusability

It’s a good idea to isolate pagination logic into a distinct class to utilize it in different parts of your application. Your code becomes more flexible and abstract as a result. Let’s expand on this idea further, starting with giving the PaginationResponse model greater generality:

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<T> {
  final MetaData meta;
  final List<T> data;


  PaginationResponse({required this.meta, required this.data});


  factory PaginationResponse.fromJson(
          Map<String, dynamic> json, T Function(Object?) dataFromJson) =>
      _$PaginationResponseFromJson(json, dataFromJson);
}


@JsonSerializable()
class MetaData {
  final int page;
  final int perPage;
  final int totalPage;


  MetaData(
      {required this.page, required this.perPage, required this.totalPage});


  factory MetaData.fromJson(Map<String, dynamic> json) =>
      _$MetaDataFromJson(json);


  Map<String, dynamic> toJson() => _$MetaDataToJson(this);
}

Abstract Controller

We might develop a mixin class called PaginationController to implement a reusable pagination controller using Riverpod in Flutter state management library. Where pagination is required, this controller will give methods to handle the logic and can be incorporated into Riverpod notifiers.How to put it into practice is as follows:

Code:

mixin PaginationController<T> on AsyncNotifierBase<PaginationResponse<T>> {
  FutureOr<PaginationResponse<T>> loadData(PaginationRequest query);


  Future<void> loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading<PaginationResponse<T>>().copyWithPrevious(oldState);
    state = await AsyncValue.guard<PaginationResponse<T>>(() async {
      final res = await loadData(oldState.requireValue.nextPage());
      res.data.insertAll(0, state.requireValue.data);
      return res;
    });
  }


  bool canLoadMore() {
    if (state.isLoading) return false;
    if (!state.hasValue) return false;
    if (state.requireValue.isCompleted) return false;
    return true;
  }
}

The following features are provided by this PaginationController mixin:

1. loadData: When merging this class, you must implement the abstract function loadData. The actual data fetching and parsing should be handled by it.

2.loadMore: This method receives new data, adds it to the existing data, and starts a loading state. It is in charge of loading more data.

3. canLoadMore: This method assesses if additional data isloaded. It determines whether a loading procedure is in progress and whether all data has been loaded.

4. isCompleted: Based on the data in the PaginationResponse model, the getter determines whether the pagination has been completed.

Code:

bool get isCompleted => meta.page >= meta.totalPage;

5. nextPage(): The next page is returned via the nextPage() method, which is found in the pagination response.

 PaginationRequest nextPage() =>
      PaginationRequest(perPage: meta.perPage, page: meta.page + 1);

Therefore, the final code is:

// model/pagination_request.dart
@JsonSerializable()
class PaginationRequest {
  final int page;
  final int perPage;


  PaginationRequest({
    required this.page,
    required this.perPage,
  });


  factory PaginationRequest.fromJson(Map<String, dynamic> json) =>
      _$PaginationRequestFromJson(json);


  Map<String, dynamic> toJson() => _$PaginationRequestToJson(this);
}

//model/pagination_response.dart

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<T> {
  final MetaData meta;
  final List<T> data;


  bool get isCompleted => meta.page >= meta.totalPage;
  PaginationRequest nextPage() =>
      PaginationRequest(perPage: meta.perPage, page: meta.page + 1);


  PaginationResponse({required this.meta, required this.data});


  factory PaginationResponse.fromJson(
          Map<String, dynamic> json, T Function(Object?) dataFromJson) =>
      _$PaginationResponseFromJson(json, dataFromJson);
}


@JsonSerializable()
class MetaData {
  final int page;
  final int perPage;
  final int totalPage;


  MetaData(
      {required this.page, required this.perPage, required this.totalPage});


  factory MetaData.fromJson(Map<String, dynamic> json) =>
      _$MetaDataFromJson(json);


  Map<String, dynamic> toJson() => _$MetaDataToJson(this);
}

//pagination_controller.dart

mixin PaginationController<T> on AsyncNotifierBase<PaginationResponse<T>> {
  FutureOr<PaginationResponse<T>> loadData(PaginationRequest query);


  Future<void> loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading<PaginationResponse<T>>().copyWithPrevious(oldState);
    state = await AsyncValue.guard<PaginationResponse<T>>(() async {
      final res = await loadData(oldState.requireValue.nextPage());
      res.data.insertAll(0, state.requireValue.data);
      return res;
    });
  }


  bool canLoadMore() {
    if (state.isLoading) return false;
    if (!state.hasValue) return false;
    if (state.requireValue.isCompleted) return false;
    return true;
  }
}

The Pagination Controller is being utilized

Let’s use the base controller we just made to begin using it in our application. To incorporate the controller into your code, follow these steps:

  • The magic takes place in the ItemsController Class. It uses the PaginationController mixin we created and extends AutoDisposeAsyncNotifier.
  • We specify the initial data fetching strategy in the construct method. Here, 30 items are displayed on the first page.To fit your needs, you can change this.
  • The task of obtaining data from the apiClient is under the loadData function. You may switch out this with the logic you use to retrieve data from a use case, repository, service, or directly through an API call.
  • The fetchItems function serves as an illustration of how the apiClient may be used to retrieve data. It utilizes Dio to submit an HTTP request and returns a PaginationResponse< item Item>.

Code:

 FutureOr<PaginationResponse<Item>> fetchItems(PaginationRequest? query) async {
    final res = await dio.get<Map>("/items", queryParameters: query?.toJson());
    return PaginationResponse<Item>.fromJson(
        res.data!.cast(), (v) => Item.fromJson(v! as Map<String, dynamic>));
  }

Deployment of Flutter Hooks

Once the pagination logic has been successfully created, the next critical step is figuring out when to use the loadMore method. For endless scroll behavior, this should be activated when the user gets close to the end of the page or clicks the “Page/Load More” button. Since managing button clicks is very simple, we shall explore the idea of limitless scrolling in this post. Using a ScrollController or NotificationListener widget will enable unlimited scroll. We will show how to utilize flutter_hooks to encapsulate this logic into a unique usePagination hook to make this even more straightforward. The good news is that it will only necessitate a small amount of code, making implementation simple.

Code:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';


ScrollController usePagination(
    VoidCallback fetchData, bool Function() isLoadMore) {
  final scrollController = useScrollController();


  void scrollListener() {
    if (isLoadMore() &&
        scrollController.position.pixels >=
            scrollController.position.maxScrollExtent) {
      fetchData();
    }
  }


  useEffect(() {
    scrollController.addListener(scrollListener);
    return null;
  }, [scrollController]);


  return scrollController;
}

Hire Flutter Experts from Flutter Agency

Pagination with cursor

The complete cursor pagination code is provided below, along with a description of the changes made:

Code:

import 'package:json_annotation/json_annotation.dart';
part "pagination_request.g.dart";


@JsonSerializable()
class PaginationRequest {
  final String? cursor;


  PaginationRequest({
    this.cursor,
  });


  factory PaginationRequest.fromJson(Map<String, dynamic> json) =>
      _$PaginationRequestFromJson(json);


  Map<String, dynamic> toJson() => _$PaginationRequestToJson(this);
}


@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<T> {
  final List<T> data;


  bool get isCompleted =>
      data.isEmpty; // if we get empty data that mean we completed


  PaginationResponse({required this.data});


  factory PaginationResponse.fromJson(
          Map<String, dynamic> json, T Function(Object?) dataFromJson) =>
      _$PaginationResponseFromJson(json, dataFromJson);
}

We tailored the isCompleted implementation, deleted the metadata, and moved the nextPage method to the controller below.

Code:

mixin PaginationController<T> on AsyncNotifierBase<PaginationResponse<T>> {
  FutureOr<PaginationResponse<T>> loadData(PaginationRequest query);
  PaginationRequest nextPage(PaginationResponse<T> current);
  Future<void> loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading<PaginationResponse<T>>().copyWithPrevious(oldState);
    state = await AsyncValue.guard<PaginationResponse<T>>(() async {
      final res = await loadData(nextPage(oldState.requireValue));
      res.data.insertAll(0, state.requireValue.data);
      return res;
    });
  }


  bool canLoadMore() {
    if (state.isLoading) return false;
    if (!state.hasValue) return false;
    if (state.requireValue.isCompleted) return false;
    return true;
  }
}

The nextPage abstract method is a significant change since we have to use the final controller to implement it.

@JsonSerializable()
class Item {
  final String id;
  final String name;
  Item(this.id, this.name);


  factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);
  Map<String, dynamic> toJson() => _$ItemToJson(this);
}


final itemsController = AsyncNotifierProvider.autoDispose<ItemsController,
    PaginationResponse<Item>>(ItemsController.new);


class ItemsController extends AutoDisposeAsyncNotifier<PaginationResponse<Item>>
    with PaginationController<Item> {
  @override
  Future<PaginationResponse<Item>> build() async {
    return await loadData(PaginationRequest(cursor: null));
  }


  @override
  FutureOr<PaginationResponse<Item>> loadData(PaginationRequest query) {
    return ref.read(itemRepoProvider).getItemByCursorPaginate(query);
  }


  @override
  PaginationRequest nextPage(PaginationResponse<Item> current) =>
      PaginationRequest(cursor: current.data.last.id);
}

The nextPage method has been moved to the controller to provide access to the item’s properties since other layers might not be familiar with the item structure.

The user interface component won’t change.

Remember that depending on how your back end handles pagination, each use case may require distinct modifications and adjustments.

Conclusion

In conclusion, using RiverPod with Hooks to implement pagination in your Flutter app is a game-changer. It guarantees effective data management and a smooth user experience. To enhance your app’s performance and user satisfaction, be bold and hire Flutter developers who are experienced in these strategies if you need assistance. Coding is fun!

Frequently Asked Questions (FAQs)

1. Define pagination in Flutter.

Flutter has a pagination feature enables users to look at big data in smaller chunks. While loading every resource at once would be difficult or slow, such as in social network feeds, search results, or product listings, pagination is frequently used.

2. Which pagination types are discussed in this article?

The two main types of pagination are covered in this article:

Page/Offset Pagination: The user specifies the desired page number or offset in this traditional method, leaving data retrieval to the back end.
Cursor/Keyset/Seek/Time-Based Pagination: With this type, data about the user’s most recent item view is sent from the front end to the back end.

3. Is isolating the pagination logic into a different class is a good idea?

The reusability and maintainability of the code are improved by separating the pagination logic into a distinct class. It lets you decouple the functionality and apply it across various app components, allowing a simpler and more modular codebase.

Book Your Flutter Developer Now

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *

Discuss Your Project

Connect with Flutter Agency's proficient skilled team for your app development projects across different technologies. We'd love to hear from you! Fill out the form below to discuss your project.

Have Project For Us

Get in Touch

"*" indicates required fields

ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields