Skip to content

Flutter package that implements infinite scroll pagination with pull to refresh functionality and pagination status updates.

License

Notifications You must be signed in to change notification settings

GammaTechMK/gamma_smart_pagination

Repository files navigation

GammaSmartPagination

Flutter package for implementing infinite scroll pagination, with support for pull to refresh.

pub package pubscore License: MIT


Table of Contents

Features

  1. Pull down to refresh
  2. Pull up / scroll to bottom to load more
  3. Different controller statuses (loading, refreshing, noMoreData, loadingFailed, refreshingFailed etc...)
  4. Widgets to indicate status (no data loaded, errors when refreshing or loading more, no more items to load etc...)

Supported platforms

  • Flutter Android
  • Flutter iOS
  • Linux (NOT TESTED)
  • Windows (NOT TESTED)
  • MacOS (NOT TESTED)
  • Fuschia (NOT TESTED)

Installation

Add gamma_smart_pagination: ^1.0.4 to your pubspec.yaml dependencies. And import it:

import 'package:gamma_smart_pagination/gamma_smart_pagination.dart';

How to use

  1. Simply create a GammaSmartPagination widget.
  2. Wrap it around any scrollable widget (ListView / GridView).
  3. Pass it the required GammaController and ScrollController along with any other available params.

Tip: Keep in mind that GammaSmartPagination is actually a SingleChildScrollView, so be mindful to not have unbounded height in the parent widget. (ListViews wrapped with GammaSmartPagination need to have shrinkWrap:true and NeverScrollableScrollPhysics.)

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

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  GammaController gammaController = GammaController();
  ScrollController scrollController = ScrollController();

  List<String> itemsList = [];
  bool isLoading = false;

  @override
  void initState() {
    fetchItems();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gamma Smart Pagination Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Example screen'),
        ),
        body: SafeArea(
          child: isLoading ? _getLoadingIndicator : _buildBody(),
        ),
      ),
    );
  }

  Widget _buildBody() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Expanded(
          // This is the important part:
          child: GammaSmartPagination(
            gammaSmartController: gammaController,
            scrollController: scrollController,
            onLoadMore: () => loadMore(),
            onRefresh: () => refreshItems(),
            itemCount: itemsList.length,
            header: Container(
              height: 300.0,
              color: Colors.orange,
            ),
            child: ListView.separated(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemBuilder: (context, index) => ListTile(
                title: Text(itemsList[index]),
              ),
              separatorBuilder: (context, index) => const Divider(),
              itemCount: itemsList.length,
            ),
          ),
        ),
      ],
    );
  }

  Widget get _getLoadingIndicator => const Center(
        child: CircularProgressIndicator(),
      );

  Future<void> fetchItems() async {
    setState(() {
      isLoading = true;
    });
    await Future.delayed(const Duration(seconds: 1));
    final fakeItems = List.generate(10, (index) => 'Item ${index + 1}');
    // Update the controller status to completed or failed based on the result
    // If it is the initial request for data, you can use the idle or failed status
    gammaController.setIdle();
    setState(() {
      itemsList = fakeItems;
      isLoading = false;
    });
  }

  Future<void> loadMore() async {
    await Future.delayed(const Duration(seconds: 1));
    final fakeItems = List.generate(10, (index) => 'Item ${itemsList.length + index + 1}');
    // Update the controller status to completed or failed based on the result
    gammaController.setLoadingCompleted();
    setState(() {
      itemsList = [...itemsList, ...fakeItems];
    });
  }

  Future<void> refreshItems() async {
    await Future.delayed(const Duration(seconds: 1));
    final fakeItems = List.generate(10, (index) => 'Item ${index + 1}');
    // Update the controller status to completed or failed based on the result
    gammaController.setRefreshingCompleted();
    setState(() {
      itemsList = fakeItems;
    });
  }

  @override
  void dispose() {
    // Don't forget to dispose the controllers
    gammaController.dispose();
    scrollController.dispose();
    super.dispose();
  }
}

Example app

For a full example check out the Example app

Params

GammaSmartPagination(
  key: const Key('firstScreenInfinitePagination'),
  // Required (for status updates)
  gammaSmartController: GammaController(),
  // Required (for triggering load more when scrolled to bottom)
  scrollController: ScrollController(),
  // The amount of space by which to trigger reload when scroll reaches the end of the list.
  scrollSensitivityTrigger: 200.0,
  // Future<void> Callback when pull to refresh is triggered
  onRefresh: () => refreshItems(),
  // Future<void> Callback when user scrolls to the bottom of the list
  onLoadMore: () => loadMore(),
  // Required
  itemCount: itemsList.length,
  // Required (Usually a ListView or GridView)
  // IMPORTANT:
  // shrinkWrap: true
  // physics: const NeverScrollableScrollPhysics(),
  //
  // This is required because the GammaSmartPagination
  // is a SingleChildScrollView, and any nested scrollable
  // should be NON-scrollable, and shrink wrap set to true
  // where needed
  child: ListView.builder(
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    itemCount: itemsList.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(itemsList[index]),
      );
    }
  ),
  // This is an optional header widget
  header: Container(
    height: 300.0,
    color: Colors.orange,
  ),
  noInitialDataWidget: Text('No data loaded'),
  noMoreDataWidget: Text('No more data to load'),
  loadingFailedWidget: Text('Failed to load more items...'),
  refreshFailedWidget: Text('Failed to refresh data...'),
  loadingIndicator: CircularProgressIndicator.adaptive(),
  // Set to true if you need to have logs in console
  // when loadMore or onRefresh are called
  enableLogging: false,
)