mo_infinite_scroll

A simple, easy-to-use infinite scroll package for Flutter with minimal required parameters, pull-to-refresh, smart pre-fetching, and full placeholder support.


Features

  • Minimal required params — just fetcher and itemBuilder
  • 🔄 Pull-to-refresh built in (standalone variant)
  • Pre-fetch the next page before the user reaches the end
  • 🛑 Loop prevention — skips fetch when list is too short or already loading
  • 📦 Sliver variant — drop into any CustomScrollView, let the parent control refresh
  • 🎨 Placeholder widgets — loading, empty, and error states, all overridable
  • 🎛️ External controller — trigger refresh() from outside the widget

Getting started

Add to your pubspec.yaml:

dependencies:
    mo_infinite_scroll: ^1.0.1

Usage

Standalone list (with pull-to-refresh)

import 'package:mo_infinite_scroll/mo_infinite_scroll.dart';

MoInfiniteScroll<Post>(
  fetcher: (page, limit) => myApi.getPosts(page: page, limit: limit),
  itemBuilder: (context, post) => PostCard(post: post),
)

With all options

MoInfiniteScroll<Post>(
  fetcher: (page, limit) => myApi.getPosts(page: page, limit: limit),
  itemBuilder: (context, post) => PostCard(post: post),
  limit: 20,                          // items per page, passed to your API
  prefetchOffset: 3,                  // pre-fetch when 3 items from the end
  loadingPlaceholder: MyShimmer(),    // first-load skeleton
  emptyPlaceholder: MyEmptyState(),   // zero items
  errorPlaceholder: MyErrorState(),   // fetch error
  loadingMoreIndicator: MySpinner(),  // bottom-of-list spinner
  separatorBuilder: (_, __) => const Divider(),
  padding: const EdgeInsets.all(16),
)

External controller (refresh from an AppBar button)

final _controller = MoInfiniteScrollController<Post>();

// In build():
Scaffold(
  appBar: AppBar(
    actions: [
      IconButton(
        icon: const Icon(Icons.refresh),
        onPressed: () => _controller.refresh(fetcher, limit),
      ),
    ],
  ),
  body: MoInfiniteScroll<Post>(
    controller: _controller,
    fetcher: fetcher,
    itemBuilder: (context, post) => PostCard(post: post),
  ),
)

Sliver variant (inside a CustomScrollView)

final _controller = MoInfiniteScrollController<Post>();

RefreshIndicator(
  onRefresh: () => _controller.refresh(
    (page, limit) => myApi.getPosts(page: page, limit: limit),
    20,
  ),
  child: CustomScrollView(
    slivers: [
      const SliverAppBar(title: Text('Posts'), floating: true),
      MoInfiniteScrollSliver<Post>(
        controller: _controller,
        fetcher: (page, limit) => myApi.getPosts(page: page, limit: limit),
        itemBuilder: (context, post) => PostCard(post: post),
      ),
    ],
  ),
)

Parameters

Shared by both widgets

Parameter Type Required Default Description
fetcher PageFetcher<T> Called for each page. Receives (page, limit). Return [] or fewer items than limit to signal end.
itemBuilder Widget Function(context, T) Builds a single list item.
limit int 20 Items per page, forwarded to fetcher.
prefetchOffset int 3 Start pre-fetching when this many items remain.
controller MoInfiniteScrollController<T> internal External controller to call refresh() from outside.
loadingPlaceholder Widget spinner Shown during the first fetch.
emptyPlaceholder Widget inbox icon Shown when the list is empty.
errorPlaceholder Widget error icon + retry Shown on fetch error.
loadingMoreIndicator Widget spinner Shown at the bottom while loading more.
separatorBuilder Widget Function(context, index) none Optional separator between items.

MoInfiniteScroll only

Parameter Type Default Description
padding EdgeInsetsGeometry none Padding for the internal ListView.
physics ScrollPhysics default Physics for the internal ListView.

How pre-fetching & loop prevention work

  • The next page is fetched when the user scrolls within prefetchOffset items of the end.
  • If the current page returned fewer items than limit, hasReachedEnd is set to true and no further fetches are made.
  • Concurrent fetches are blocked — if a fetch is already in progress, new calls to fetchNextPage are ignored.
  • On error, further fetches are blocked until the user retries.

License

MIT