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