Flutter package for implementing infinite scroll pagination, with support for pull to refresh.
- Pull down to refresh
 - Pull up / scroll to bottom to load more
 - Different controller statuses (loading, refreshing, noMoreData, loadingFailed, refreshingFailed etc...)
 - Widgets to indicate status (no data loaded, errors when refreshing or loading more, no more items to load etc...)
 
- Flutter Android
 - Flutter iOS
 - Linux (NOT TESTED)
 - Windows (NOT TESTED)
 - MacOS (NOT TESTED)
 - Fuschia (NOT TESTED)
 
Add gamma_smart_pagination: ^1.0.4 to your pubspec.yaml dependencies. And import it:
import 'package:gamma_smart_pagination/gamma_smart_pagination.dart';- Simply create a 
GammaSmartPaginationwidget. - Wrap it around any scrollable widget (ListView / GridView).
 - Pass it the required 
GammaControllerandScrollControlleralong with any other available params. 
Tip: Keep in mind that
GammaSmartPaginationis actually aSingleChildScrollView, so be mindful to not have unbounded height in the parent widget. (ListViews wrapped with GammaSmartPagination need to haveshrinkWrap:trueandNeverScrollableScrollPhysics.)
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();
  }
}For a full example check out the Example app
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,
)