Scroll Smarter, Not Harder: Generic Pagination in Flutter Explained

Written by dhruvam | Published 2025/05/07
Tech Story Tags: flutter | pagination-in-flutter | flutter-pagination | bloc | generics | flutter-bloc-pagination | generic-pagination-flutter | flutter-infinite-scroll

TLDRLearn how to build pagination in Flutter with Generics that work for any kind of list.via the TL;DR App

Learn how to build pagination in Flutter with Generics.

Chapters:

  1. Learn Data Structures the fun way: Anagram Game

  2. Scarne’s Dice: A fun way to learn Flutter and Bloc

  3. Pagination in Flutter with Generics and Bloc: Write Once and use Anywhere

  4. More to come …

In the previous workshops, we worked on Lists, HashMaps, and HashSets and learned State Management with Bloc. In this workshop, we will build Pagination in Flutter.

TLDR: Code for Generic Pagination

Contents:

  1. Who is the target Audience for this article?
  2. Why write on Pagination?
  3. What is our end goal?
  4. Building core files for this project
  5. Building the UI
  6. Create ScrollEndMixin
  7. Building the bloc files
  8. Integrating the bloc into UI
  9. Abstracting the bloc and the logic
  10. Let’s put everything together

Who is the target audience for this article?

Easy peasy, this article aims to help newcomers and intermediates. Experts already know this. They have probably moved on to the next chapter.

This article is for those who are still learning and looking for better examples to learn from.

There is one more faction that this article is for. The ones who have developed something far more efficient. Help us all and paste your solution, and explain why your code is better.

Why write on Pagination?

There are already many articles and write-ups explaining pagination. So how does this do anything for me?

So this article aims to build better protocols for writing code and understanding why to use generics.

Since this is in continuation of our existing series, from Learn Data Structures the fun way: Anagram Game, this article assumes you know how to set up a project, how to use Bloc.

What is our end goal?

Our end goal is simple. We want to build a solution that can be applied to any kind of list and can load more data on demand.

Building core files for this project

Let’s build a core file and some utility files that I am using for this workshop.

import 'package:equatable/equatable.dart';
// T is type of data
// H is type of error
sealed class DataField<T,H> extends Equatable {
  const DataField();
}

class DataFieldInitial<T, H> extends DataField<T, H> {
  const DataFieldInitial();
  @override
  List<Object?> get props => [];
}
class DataFieldLoading<T, H> extends DataField<T, H> {
  const DataFieldLoading(this.data);
  final T data;

  @override
  List<Object?> get props => [data];
}
class DataFieldSuccess<T, H> extends DataField<T, H> {
  const DataFieldSuccess(this.data);
  final T data;

  @override
  List<Object?> get props => [data];
}

class DataFieldError<T, H> extends DataField<T, H> {
  const DataFieldError(this.error, this.data);
  final H error;
  final T data;

  @override
  List<Object?> get props => [error, data];
}

  • DataField is a sealed class that we can use to store the states of the data being stored. Learn more about class modifiers here.
  • We have defined 4 states for the field, which can be initial , loading , success and error
  • We have declared the DataField class with two types, T and H where T is the type of data that we will be storing and H being the type of error.

import 'package:flutter/material.dart';

extension ColorToHex on Color {
  /// Converts the color to a 6-digit hexadecimal string representation (without alpha).
  String toHex({bool leadingHash = false}) {
    return '${leadingHash ? '#' : ''}'
        '${red.toRadixString(16).padLeft(2, '0')}'
        '${green.toRadixString(16).padLeft(2, '0')}'
        '${blue.toRadixString(16).padLeft(2, '0')}';
  }
}

class ColorGenerator {
  /// Generates a pseudo-random color based on the provided index.
  ///
  /// This function uses a simple hash function to map the index to a unique
  /// color. It's not cryptographically secure, but it's sufficient for
  /// generating distinct colors for UI elements.
  ///
  /// The generated color will always have full opacity (alpha = 0xFF).
  static Color generateColor(int index) {
    // A simple hash function to distribute the index across the color space.
    int hash = index * 0x9E3779B9; // A large prime number
    hash = (hash ^ (hash >> 16)) & 0xFFFFFFFF; // Ensure 32-bit unsigned

    // Extract color components from the hash.
    int r = (hash >> 16) & 0xFF;
    int g = (hash >> 8) & 0xFF;
    int b = hash & 0xFF;

    return Color(0xFF000000 + (r << 16) + (g << 8) + b); // Full opacity
  }
}

  • This is an optional file, and we are using this for building our Product Model Objects.
  • I hope the comments in the code are sufficient for the same. If not, please comment.

import 'package:equatable/equatable.dart';
import 'package:pagination_starter/core/color_generator.dart';

// This is how the model will look like
class Product extends Equatable {
  const Product({required this.id, required this.name, required this.image});
  factory Product.fromInteger(int index) {
    final randomColorHexCode = ColorGenerator.generateColor(index).toHex();
    return Product(
      id: index.toString(),
      name: 'Item number: $index',
      image: 'https://dummyjson.com/image/400x200/${randomColorHexCode}/?type=webp&text=With+Id+$index&fontFamily=pacifico',
    );
  }
  final String id;
  final String name;
  final String image;

  @override
  List<Object?> get props => [id];
}

class ProductResponse extends Equatable {
  const ProductResponse({required this.products, required this.time});
  final List<Product> products;
  final DateTime time;

  @override
  List<Object?> get props => [products, time];
}

These will be the models used in this workshop, assuming the response to the API looks something like this:

{

  "statusCode": 200,
  "time": DateTime.now(),
  "products": [
    {
      "id": 1,
      "name": "Index : 1",
      "image": "image_1.webp"
    },
    {
      "id": 2,
      "name": "Index : 2",
      "image": "image_2.webp"
    },
    ...
  ],
},

You can ignore these files, too.

import 'package:pagination_starter/core/models.dart';

class ApiClient {
// Used for mocking api response
  Future<ProductResponse> getProducts({int? page, int pageSize = 10}) async {
    await Future<void>.delayed(const Duration(seconds: 2));

    final start = ((page ?? 1) - 1) * pageSize + 1;
    return ProductResponse(
      products: List.generate(
        pageSize,
        (index) {
          return Product.fromInteger(start + index);
        },
      ),
      time: DateTime.now(),
    );
  }
}

Building the UI

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pagination_starter/core/api_client.dart';
import 'package:pagination_starter/core/bloc/pagination_bloc.dart';
import 'package:pagination_starter/core/models.dart';
import 'package:pagination_starter/counter/bloc/products_bloc.dart';
import 'package:pagination_starter/l10n/l10n.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const CounterView();
  }
}

class CounterView extends StatefulWidget {
  const CounterView({super.key});

  @override
  State<CounterView> createState() => _CounterViewState();
}

class _CounterViewState extends State<CounterView> {

    // This function takes in scroll notifications and notifies
    // when the scroll has reached
    void onScroll(ScrollNotification notification, {VoidCallback? onEndReached}) {
    if (_isAtBottom(notification)) {
      // you can paginate
      onEndReached?.call();
    }
  }

  bool _isAtBottom(ScrollNotification notification) {
    final maxScrollExtent = notification.metrics.maxScrollExtent;
    final currentScrollExtent = notification.metrics.pixels;
    // you can play around with this number
    const paginationOffset = 200;
    return currentScrollExtent >= maxScrollExtent - paginationOffset;
  }

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    return Scaffold(
      appBar: AppBar(title: Text(l10n.counterAppBarTitle)),
      body: NotificationListener<ScrollNotification>(
        onNotification: (notification) {
          onScroll(
            notification,
            onEndReached: () {
              // send a call to fetch the new set of items
            },
          );
          return false;
        },
        child: _list(
          // fake products
          List.generate(10, Product.fromInteger),
          true,
        ),
      ),
    );
  }

  Widget _list(List<Product> data, bool isPaginating) {
    return ListView.builder(
      itemBuilder: (context, index) => _buildItem(
        isPaginating,
        index == data.length,
        index >= data.length? null: data[index],
      ),
      itemCount: data.length + (isPaginating ? 1 : 0),
    );
  }

  Widget _buildItem(bool isPaginating, bool isLastItem, Product? product) {
    if (isPaginating && isLastItem) {
      return const Center(
        child: Padding(
          padding: EdgeInsets.all(20),
          child: CircularProgressIndicator(),
        ),
      );
    }
    return Card(
      child: Column(
        children: [
          Image.network(product!.image),
        ],
      ),
    );
  }
}

  • _buildItem function builds the item of the list. It can be either a card with an Image or a loading indicator, depending on whether it is the last index and the list is paginating.
  • _list function builds the list of items. We add 1 to the item count to accommodate for paginating loader.
  • We have also added a NotificationListener widget that listens to the scroll notifications to indicate when we have reached the end of the list.
  • Since our UI class holds some data that can be offloaded to a mixin, and will add re-usability in our code.

Create ScrollEndMixin

You can skip this part and move ahead if you just want to add some code to get your pagination working.

import 'package:flutter/widgets.dart';

mixin BottomEndScrollMixin {
    // you can play around with this number
    double _paginationOffset = 200;

    void onScroll(ScrollNotification notification, {VoidCallback? onEndReached}) {
    if (_isAtBottom(notification)) {
      // you can paginate
      onEndReached?.call();
    }
  }

  set paginationOffset(double value) => _paginationOffset = value;

  bool _isAtBottom(ScrollNotification notification) {
    final maxScrollExtent = notification.metrics.maxScrollExtent;
    final currentScrollExtent = notification.metrics.pixels;
    return currentScrollExtent >= maxScrollExtent - _paginationOffset;
  }
}

  • In this mixin, we find when the scroll has reached the bottom end.

  • We pass in a callback, which, when called, indicates that the end has been reached, can proceed with your action.

  • We can also change the value of the pagination offset.

Now let’s piece this together with the UI

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pagination_starter/core/api_client.dart';
import 'package:pagination_starter/core/bloc/pagination_bloc.dart';
import 'package:pagination_starter/core/bottom_end_scroll_mixin.dart';
import 'package:pagination_starter/core/models.dart';
import 'package:pagination_starter/counter/bloc/products_bloc.dart';
import 'package:pagination_starter/l10n/l10n.dart';

...

class _CounterViewState extends State<CounterView> with BottomEndScrollMixin {

  @override
  void initState() {
    // set scroll offset in pixels before which scroll should happen
    paginationOffset = 200;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    return Scaffold(
      appBar: AppBar(title: Text(l10n.counterAppBarTitle)),
      body: NotificationListener<ScrollNotification>(
        onNotification: (notification) {
          onScroll(
            notification,
            onEndReached: () {
              // send a call to fetch the new set of items
            },
          );
          return false;
        },
        child: _list(
          List.generate(10, Product.fromInteger),
          true,
        ),
    );
  }

  ...
}

This is how the app looks right now.

At this stage, our app works and paginates too. But the paginate call does not do anything. Let’s start with the logic. And neither are the products real.

Building the Bloc Files

part of 'products_bloc.dart';

abstract class ProductEvent extends Equatable {
  const ProductEvent();
}

class GetProducts extends ProductEvent {
  const GetProducts(this.page);
  final int page;
  @override
  List<Object?> get props => [page];
}

  • We have created only one event for now, GetProducts which takes in the page number to load. This page number can be anything.

part of 'products_bloc.dart';

class ProductsState extends Equatable {
  const ProductsState({
    required this.page,
    required this.products,
    required this.canLoadMore,
  });
  factory ProductsState.initial() => const ProductsState(
        // page is always incremented when
        // it's sent, so starting from 0.
        page: 0,
        products: DataFieldInitial(),
        canLoadMore: true,
      );
  final int page;
  final DataField<List<Product>, String> products;
  final bool canLoadMore;

  ProductsState copyWith({
    int? page,
    DataField<List<Product>, String>? products,
    bool? canLoadMore,
  }) {
    return ProductsState(
      page: page ?? this.page,
      products: products ?? this.products,
      canLoadMore: canLoadMore ?? this.canLoadMore,
    );
  }

  @override
  List<Object?> get props => [products, page, canLoadMore];
}

  • ProductsState class holds the state of the pagination.

  • page defines the current page number. If your pagination uses a String, you can use a String here for pagination.

  • products holds the state of the products, whether they are in a loading state, initial, success or error.

  • canLoadMore defines the breaker for pagination.

import 'dart:async';

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pagination_starter/core/api_client.dart';
import 'package:pagination_starter/core/data_field.dart';
import 'package:pagination_starter/core/models.dart';

part 'product_event.dart';
part 'products_state.dart';

class ProductsBloc extends Bloc<ProductEvent, ProductsState> {
  ProductsBloc(this.apiClient): super(ProductsState.initial()) {
    on<GetProducts>(_fetchProducts);
  }

  final ApiClient apiClient;

  FutureOr<void> _fetchProducts(GetProducts event, Emitter<ProductsState> emit) async {
    // check if it is already loading, if it is, return
    if (state.products is DataFieldLoading) return;
    // check if we can load more results
    if (!state.canLoadMore) return;

    final fetchedProducts = switch (state.products) {
      DataFieldInitial<List<Product>, String>() => <Product>[],
      DataFieldLoading<List<Product>, String>(:final data) => data,
      DataFieldSuccess<List<Product>, String>(:final data) => data,
      DataFieldError<List<Product>, String>(:final data) => data,
    };

    // start loading state
    emit(
      state.copyWith(
        products: DataFieldLoading<List<Product>, String>(fetchedProducts),
      ),
    );

    // fetch results
    final results = await apiClient.getProducts(page: event.page, pageSize: 10);
    // check if products are returned empty
    // if they are, stop pagination
    if (results.products.isEmpty) {
      emit(
        state.copyWith(
          canLoadMore: false,
        ),
      );
    }

    final products = [...fetchedProducts, ...results.products];

    // increment the page number and update data
    emit(
      state.copyWith(
        page: event.page,
        products: DataFieldSuccess(products),
      ),
    );
  }
}

  • ProductsBloc uses ApiClient that we created earlier in the core files. This is a mock dependency for fetching products.
  • We register the GetProducts event handler to define the logic for initial and subsequent calls.
  • We stop the pagination if we are already fetching the data or if we have received a breaker for stopping the pagination.
  • We start the loading state to show the loader in the UI.
  • Fetch the results using the ApiClient and if the products returned are empty, this means no more calls to pagination are required, so we can end the pagination here.
  • Once all the data is fetched, we update the state with Success. For now handling the error state for initial and subsequent pagination, I leave that up to you. If you still want the solution, hit me up in the comments. Will add it there.

Integrating the Bloc into UI

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pagination_starter/core/api_client.dart';
import 'package:pagination_starter/core/bottom_end_scroll_mixin.dart';
import 'package:pagination_starter/core/data_field.dart';
import 'package:pagination_starter/core/models.dart';
import 'package:pagination_starter/counter/bloc/products_bloc.dart';
import 'package:pagination_starter/l10n/l10n.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) =>
          ProductsBloc(ApiClient())..add(const GetProducts(1)),
      child: const CounterView(),
    );
  }
}

...

class _CounterViewState extends State<CounterView> with BottomEndScrollMixin {

  ...

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    final bloc = context.read<ProductsBloc>();
    return Scaffold(
      appBar: AppBar(title: Text(l10n.counterAppBarTitle)),
      body: NotificationListener<ScrollNotification>(
        onNotification: (notification) {
          onScroll(
            notification,
            onEndReached: () {
              // send a call to fetch the new set of items
              bloc.add(GetProducts(bloc.state.page + 1));
            },
          );
          return false;
        },
        child: BlocBuilder<ProductsBloc, ProductsState>(
          builder: (context, state) {
            return Center(
              child: switch (state.products) {
                DataFieldInitial<List<Product>, String>() =>
                  const CircularProgressIndicator(),
                DataFieldLoading<List<Product>, String>(:final data) =>
                  _list(data, true),
                DataFieldSuccess<List<Product>, String>(:final data) =>
                  data.isEmpty
                      ? const Text('No more products found')
                      : _list(data, false),
                DataFieldError<List<Product>, String>(
                  :final error,
                  :final data
                ) =>
                  data.isEmpty ? Text(error) : _list(data, false),
              },
            );
          },
        ),
      ),
    );
  }
  ...
}

  • Now integrating ProductsBloc into the UI seems easy. We add the GetProducts to the bloc to fetch the first batch of items.
  • And a second call for GetProducts in the onEndReached callback with adding 1 to the page count required for supporting this type of pagination.
  • And then we use the sealed class exhaustive mapping feature to return a Widget depending on the state of the product's field.
  • All the rest of the code is the same.

Abstracting the Bloc and the logic

We have reached the end of our story, building a Generic Pagination Solution. Since we have already created the logic for pagination, the next steps are going to be easy.

Let’s create 3 more bloc files. Same kind of files that we created earlier. And we will just copy and paste the code and change it a little to handle all kinds of data. And let’s delete some files too.

part of 'pagination_bloc.dart';

// Base event for triggering pagination fetches.
// The ID refers to the current page, or last item id
sealed class PaginationEvent<ID> extends Equatable {
  const PaginationEvent();

  @override
  List<Object?> get props => [];
}

class PaginateFetchEvent<ID> extends PaginationEvent<ID> {
  const PaginateFetchEvent(this.id);
  final ID id;

  @override
  List<Object?> get props => [id];
}

  • This is a new class. PaginationEvent class. Copy the code from the ProductEvent class and paste it here.

  • You will notice we have only added an ID type that will be your type of data used for pagination. This can be any type.

  • Delete your ProductEvent class.

part of 'pagination_bloc.dart';

class PaginationState<ID, ITEM, E> extends Equatable {
  const PaginationState({
    required this.canLoadMore,
    required this.itemState,
    required this.page,
  });

  factory PaginationState.initial(
    ID id, {
    bool canLoadMore = true,
    DataField<List<ITEM>, E> state = const DataFieldInitial(),
  }) =>
      PaginationState(
        canLoadMore: canLoadMore,
        itemState: state,
        page: id,
      );

  // This variable will hold all the states of initial and future fetches
  final DataField<List<ITEM>, E> itemState;
  final bool canLoadMore;
  // this variable is used to fetch the new batch of items
  // this can be anything from page, last item's id as offset
  // or anything or adjust as you see fit
  final ID page;

  PaginationState<ID, ITEM, E> copyWith({
    ID? page,
    DataField<List<ITEM>, E>? itemState,
    bool? canLoadMore,
  }) {
    return PaginationState(
      page: page ?? this.page,
      itemState: itemState ?? this.itemState,
      canLoadMore: canLoadMore ?? this.canLoadMore,
    );
  }

  @override
  List<Object?> get props => [page, canLoadMore, itemState];
}

  • A very similar class to ProductsState

  • Just change the type of the data and make it generic. We have used ID for the type of data for pagination, ITEM for the type of data that we will be holding a list of, and page representing the current page of the state.

  • Delete your ProductsState class.

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pagination_starter/core/data_field.dart';

part 'pagination_event.dart';
part 'pagination_state.dart';

abstract class PaginationBloc<ID, ITEM, E>
    extends Bloc<PaginationEvent<ID>, PaginationState<ID, ITEM, E>> {

  PaginationBloc({required ID page}) : super(PaginationState.initial(page)) {
    on<PaginateFetchEvent<ID>>((event, emit) async {
       // check if it is already loading, if it is, return
    if (state.itemState is DataFieldLoading) return;
    // check if we can load more results
    if (!state.canLoadMore) return;

    final fetchedProducts = switch (state.itemState) {
      DataFieldInitial<List<ITEM>, E>() => <ITEM>[],
      DataFieldLoading<List<ITEM>, E>(:final data) => data,
      DataFieldSuccess<List<ITEM>, E>(:final data) => data,
      DataFieldError<List<ITEM>, E>(:final data) => data,
    };

    // start loading state
    emit(
      state.copyWith(
        itemState: DataFieldLoading<List<ITEM>, E>(fetchedProducts),
      ),
    );

    // fetch results
    final results = await fetchNext(page: event.id);
    // check if products are returned empty
    // if they are, stop pagination
    if (results.$1.isEmpty) {
      emit(
        state.copyWith(
          canLoadMore: false,
        ),
      );
    }

    final products = [...fetchedProducts, ...results.$1];

    // increment the page number and update data
    emit(
      state.copyWith(
        page: event.id,
        itemState: DataFieldSuccess(products),
      ),
    );
    });
  }
  // Abstract method to fetch the next page of data. This is where the
  // data-specific logic goes.  The BLoC doesn't know *how* to fetch the data,
  // it just knows *when* to fetch it.
  FutureOr<(List<ITEM>, E?)> fetchNext({ID? page});
}

  • This is also almost all copy-pasted code from ProductsBloc.
  • As you can see almost all the code is the same except for the types of the data.
  • We have also added a new abstract method, fetchNext, to be implemented by the implementing Bloc.
  • This method returns a list of newly fetched items and a nullable error.

Let’s put everything together

Now that we are done with the generics and abstracting our pagination logic, we can start by making changes in our UI.

import 'dart:async';

import 'package:equatable/equatable.dart';
import 'package:pagination_starter/core/api_client.dart';
import 'package:pagination_starter/core/bloc/pagination_bloc.dart';
import 'package:pagination_starter/core/data_field.dart';
import 'package:pagination_starter/core/models.dart';


part 'product_event.dart';
part 'products_state.dart';

class ProductsBloc extends PaginationBloc<int, Product, String> {
  ProductsBloc(this.apiClient) : super(page: 1);
  final ApiClient apiClient;

  @override
  FutureOr<(List<Product>, String?)> fetchNext({int? page}) async {
    // define: how to parse the products
    return ((await apiClient.getProducts(page: page)).products, null);
  }
}

  • ProductsBloc class now extends PaginationBloc<int, Product, String> representing, int as page, Product as the list of items and String as the type of error.

  • We just need to implement the abstract fetchNext method that defines how the pages are fetched.

  • Our code is now dramatically reduced for writing the pagination for a list of items.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pagination_starter/core/api_client.dart';
import 'package:pagination_starter/core/bloc/pagination_bloc.dart';
import 'package:pagination_starter/core/bottom_end_scroll_mixin.dart';
import 'package:pagination_starter/core/data_field.dart';
import 'package:pagination_starter/core/models.dart';
import 'package:pagination_starter/counter/bloc/products_bloc.dart';
import 'package:pagination_starter/l10n/l10n.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) =>
          ProductsBloc(ApiClient())..add(const PaginateFetchEvent<int>(1)),
      child: const CounterView(),
    );
  }
}

...

class _CounterViewState extends State<CounterView> with BottomEndScrollMixin {

  ...  

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    final bloc = context.read<ProductsBloc>();
    return Scaffold(
      appBar: AppBar(title: Text(l10n.counterAppBarTitle)),
      body: NotificationListener<ScrollNotification>(
        onNotification: (notification) {
          onScroll(
            notification,
            onEndReached: () {
              // send a call to fetch the new set of items
              bloc.add(PaginateFetchEvent(bloc.state.page + 1));
            },
          );
          return false;
        },
        child: BlocBuilder<ProductsBloc, PaginationState<int, Product, String>>(
          builder: (context, state) {
            return Center(
              child: switch (state.itemState) {
                DataFieldInitial<List<Product>, String>() =>
                  const CircularProgressIndicator(),
                DataFieldLoading<List<Product>, String>(:final data) =>
                  _list(data, true),
                DataFieldSuccess<List<Product>, String>(:final data) =>
                  data.isEmpty
                      ? const Text('No more products found')
                      : _list(data, false),
                DataFieldError<List<Product>, String>(
                  :final error,
                  :final data
                ) =>
                  data.isEmpty ? Text(error) : _list(data, false),
              },
            );
          },
        ),
      ),
    );
  }
  ...
}

  • We only had to make 2 changes to incorporate the new ProductsBloc .

  • Change the type of event for fetching the data. The type had to change from GetProducts to PaginateFetchEvent.

  • Change the BlocBuilder to use the new PaginationState .



Written by dhruvam | Google-certified Android Developer @ToTheNew. Flutter Geek. Dancer. 🕺 Reader. Coffee Addict. 😍
Published by HackerNoon on 2025/05/07