US Office

1176 Shadeville Rd, Crawfordville Florida 32327, USA

 +1 (850) 780-1313

India Office

Office No. 501, Shree Ugati Corporate Park, Gandhinagar - 382421, Gujarat, India

[email protected]

Riverpod and Hooks: Unlocking the Power of Pagination

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 {
  final MetaData meta;
  final List data;


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


  factory PaginationResponse.fromJson(
          Map json) =>
      _$PaginationResponseFromJson(json);


Map 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 {
  final MetaData meta;
  final List data;


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


  factory PaginationResponse.fromJson(
          Map 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 json) =>
      _$MetaDataFromJson(json);


  Map 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 on AsyncNotifierBase> {
  FutureOr> loadData(PaginationRequest query);


  Future loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading>().copyWithPrevious(oldState);
    state = await AsyncValue.guard>(() 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 json) =>
      _$PaginationRequestFromJson(json);


  Map toJson() => _$PaginationRequestToJson(this);
}

//model/pagination_response.dart

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse {
  final MetaData meta;
  final List 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 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 json) =>
      _$MetaDataFromJson(json);


  Map toJson() => _$MetaDataToJson(this);
}

//pagination_controller.dart

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


  Future loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading>().copyWithPrevious(oldState);
    state = await AsyncValue.guard>(() 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> fetchItems(PaginationRequest? query) async {
    final res = await dio.get("/items", queryParameters: query?.toJson());
    return PaginationResponse.fromJson(
        res.data!.cast(), (v) => Item.fromJson(v! as Map));
  }

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;
}

Flutter Developers 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 json) =>
      _$PaginationRequestFromJson(json);


  Map toJson() => _$PaginationRequestToJson(this);
}


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


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


  PaginationResponse({required this.data});


  factory PaginationResponse.fromJson(
          Map 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 on AsyncNotifierBase> {
  FutureOr> loadData(PaginationRequest query);
  PaginationRequest nextPage(PaginationResponse current);
  Future loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading>().copyWithPrevious(oldState);
    state = await AsyncValue.guard>(() 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 json) => _$ItemFromJson(json);
  Map toJson() => _$ItemToJson(this);
}


final itemsController = AsyncNotifierProvider.autoDispose>(ItemsController.new);


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


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


  @override
  PaginationRequest nextPage(PaginationResponse 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

Post a Comment