mo_infinite_scroll 2.0.0
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
fetcheranditemBuilder - 🔄 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()orretry()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,
physicsdefaults toAlwaysScrollableScrollPhysicsso 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
prefetchOffsetitems of the end. - If the current page returned fewer items than
limit,hasReachedEndis set totrueand no further fetches are made. - Concurrent fetches are blocked — if a fetch is already in progress, new calls to
fetchNextPageare 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