mo_infinite_scroll 2.0.0 copy "mo_infinite_scroll: ^2.0.0" to clipboard
mo_infinite_scroll: ^2.0.0 copied to clipboard

Infinite scroll for Flutter with pull-to-refresh, pre-fetch, horizontal lists, error recovery, a Sliver variant, and customisable placeholders.

mo_infinite_scroll #

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


Features #

  • Minimal required params — just fetcher and itemBuilder
  • 🔄 Pull-to-refresh built in (vertical standalone variant)
  • ↔️ Horizontal lists — set scrollDirection: Axis.horizontal
  • Pre-fetch the next page before the user reaches the end
  • 🛟 Error recovery — retry buttons for both first-page and load-more errors, without losing loaded items
  • 🛑 Loop & race prevention — concurrent fetches are blocked, and stale responses are discarded on refresh
  • 📦 Sliver variant — drop into any CustomScrollView, let the parent control refresh
  • 🎨 Placeholder widgets — loading, empty, and error states, all overridable
  • 🎛️ External controller — trigger refresh() or retry() from anywhere

Getting started #

Add to your pubspec.yaml:

dependencies:
    mo_infinite_scroll: ^2.0.0

Then import the single entry point:

import 'package:mo_infinite_scroll/mo_infinite_scroll.dart';

Usage #

Standalone list (with pull-to-refresh) #

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

Horizontal list #

SizedBox(
  height: 160,
  child: MoInfiniteScroll<Post>(
    fetcher: (page, limit) => myApi.getPosts(page: page, limit: limit),
    itemBuilder: (context, post) => PostCard(post: post),
    scrollDirection: Axis.horizontal,
  ),
)

Pull-to-refresh is automatically disabled for horizontal lists (a Material RefreshIndicator only responds to vertical drags). Use an external controller and call controller.refresh() if you need to refresh one.

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(),   // first-page fetch error
  loadingMoreIndicator: MySpinner(),  // end-of-list spinner
  errorMoreIndicator: MyErrorRow(),   // end-of-list fetch error
  separatorBuilder: (_, __) => const Divider(),
  scrollDirection: Axis.vertical,
  reverse: false,
  shrinkWrap: false,
  pullToRefresh: true,
  scrollController: myScrollController,
  padding: const EdgeInsets.all(16),
)

External controller (refresh from an AppBar button) #

The widget attaches its fetcher and limit to the controller for you, so refresh(), retry(), and fetchNextPage() take no arguments.

final _controller = MoInfiniteScrollController<Post>();

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

Sliver variant (inside a CustomScrollView) #

final _controller = MoInfiniteScrollController<Post>();

RefreshIndicator(
  onRefresh: _controller.refresh,
  child: CustomScrollView(
    physics: const AlwaysScrollableScrollPhysics(),
    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() / retry() 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 when the first page fails.
loadingMoreIndicator Widget spinner Shown at the end while loading more.
errorMoreIndicator Widget message + retry Shown at the end when loading more fails.
separatorBuilder Widget Function(context, index) none Optional separator between items.

MoInfiniteScroll only #

Parameter Type Default Description
scrollDirection Axis Axis.vertical List axis. Horizontal disables pull-to-refresh.
reverse bool false Whether the list scrolls in reverse.
shrinkWrap bool false Whether the list sizes itself to its children.
pullToRefresh bool true Wraps the list in a RefreshIndicator (vertical only).
scrollController ScrollController none Controller for the internal ListView.
padding EdgeInsetsGeometry none Padding for the internal ListView.
physics ScrollPhysics see note Physics for the internal ListView (see note below).

Note: when pull-to-refresh is active, physics defaults to AlwaysScrollableScrollPhysics so short lists can still be pulled.

MoInfiniteScrollController #

Member Description
items Read-only view of all loaded items.
itemCount Number of loaded items.
isLoading Whether a fetch is in progress.
hasError Whether the last fetch failed.
lastError The error thrown by the last failed fetch.
hasReachedEnd Whether the last page returned fewer items than limit.
refresh() Clears everything and reloads page 1. Stale in-flight responses are discarded.
retry() Clears the error state and fetches the failed page again.
fetchNextPage() Fetches the next page (no-op while loading, on error, or at the end).

How pre-fetching, error recovery & 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.
  • If refresh() is called while a fetch is in flight, the stale response is discarded instead of being mixed into the fresh list.
  • On error, further automatic fetches are paused. A retry button is shown: full-screen if the first page failed, or as a compact row at the end of the list if a later page failed — already-loaded items are never lost.

License #

MIT

2
likes
160
points
35
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Infinite scroll for Flutter with pull-to-refresh, pre-fetch, horizontal lists, error recovery, a Sliver variant, and customisable placeholders.

Homepage
Repository (GitHub)
View/report issues

Topics

#infinite-scroll #pagination #listview #lazy-loading #pull-to-refresh

License

MIT (license)

Dependencies

flutter

More

Packages that depend on mo_infinite_scroll