Infinite Scroll List View
A powerful, flexible, and efficient infinite scrolling list view for Flutter — with built-in sorting, deduplication, dynamic mutations, sliver support, and a minimal API that stays out of your way.
Why this package?
Most infinite-scroll packages just load pages. This one manages your entire list lifecycle:
- Pages load automatically as the user scrolls — zero boilerplate.
- Items inserted mid-session land in the right sorted position instantly.
- Duplicates are detected and merged, not doubled.
- State (
isLoading,isEndOfResults) is readable at any time without subscribing to streams. - Works as a regular widget or as a sliver inside a
CustomScrollView.
Features
| Automatic pagination | New pages load as the sentinel scrolls into view |
| Smart end-of-page detection | pageSize, isEndOfPage, or the default empty-page heuristic |
| Custom comparator | Full control over sort order — items insert at the right position every time |
Deduplication + pick |
Zero == same item; choose which version to keep |
| Dynamic mutations | add(), update(), removeItem(), removeWhere() — all animated |
| State control | clear(), replace(), reload(), isLoading, isEndOfResults via GlobalKey |
| Sliver variant | SliverInfiniteScrollListView composes with app bars, headers, grids |
| One-shot loader | .once constructor — no index checks, no empty-page round-trip |
| Simple builder | simpleBuilder drops unused index and animation params |
| Separators | separatorBuilder — like ListView.separated, but paginated |
| Remove animations | removeAnimationBuilder receives the removed item for exit transitions |
| Pull-to-refresh | Built-in RefreshIndicator, opt out with refreshable: false |
| Error handling | errorBuilder for full-page errors, elementErrorBuilder for inline retry |
| Horizontal lists | .horizontal named constructor |
| Batch insert fast-path | No betweenItemRenderDelay? All items insert in a single frame |
Installation
dependencies:
infinite_scroll_list_view: ^1.3.0
flutter pub get
Quick start
Paginated list
InfiniteScrollListView<Post>(
pageSize: 20,
pageLoader: (page) => api.fetchPosts(page: page, size: 20),
elementBuilder: simpleBuilder((context, post) => PostTile(post)),
)
Single-page list (load once, done)
No more if (index == 0) fetch else return []:
InfiniteScrollListView.once(
oncePageLoader: () => api.fetchFeatured(),
elementBuilder: simpleBuilder((context, post) => PostTile(post)),
)
Inside a CustomScrollView
CustomScrollView(
slivers: [
SliverAppBar(title: Text('Feed')),
SliverInfiniteScrollListView<Post>(
pageSize: 20,
pageLoader: (page) => api.fetchPosts(page: page, size: 20),
elementBuilder: simpleBuilder((context, post) => PostTile(post)),
),
],
)
Sorting and deduplication
Provide a comparator and items always land in the right position — whether they come from pagination or a live push.
// Positive → a before b. Zero → same item (triggers update). Negative → a after b.
int compareByDate(Post a, Post b) => b.createdAt.compareTo(a.createdAt); // newest first
When two items compare as equal, the pick function decides which version to keep:
pick: (current, incoming) => incoming.updatedAt > current.updatedAt ? incoming : current,
Live mutations
Use a GlobalKey to mutate the list from anywhere — items animate in and out automatically.
final key = GlobalKey<InfiniteScrollListViewState<Message>>();
// Elsewhere — e.g. a WebSocket handler:
key.currentState!.add(newMessage); // inserts at the sorted position
key.currentState!.update(editedMessage); // updates in-place
key.currentState!.removeItem(message); // animated removal
key.currentState!.reload(); // clears and re-fetches from page 0
State control
if (key.currentState!.isLoading) showSpinner();
if (key.currentState!.isEndOfResults) showEndBanner();
await key.currentState!.clear(); // empty list, reset pagination, no fetch
await key.currentState!.replace(items); // swap contents without animation
Full example — live chat feed
class ChatScreen extends StatefulWidget { ... }
class _ChatScreenState extends State<ChatScreen> {
final _key = GlobalKey<InfiniteScrollListViewState<Message>>();
@override
void initState() {
super.initState();
socket.onMessage((msg) => _key.currentState?.add(msg));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Chat')),
body: InfiniteScrollListView<Message>(
key: _key,
pageSize: 30,
pageLoader: (page) => api.fetchMessages(page: page, size: 30),
comparator: (a, b) => b.sentAt.compareTo(a.sentAt), // newest first
pick: (current, incoming) => incoming,
elementBuilder: simpleBuilder((context, msg) => MessageBubble(msg)),
errorBuilder: (context, error) => RetryWidget(onRetry: () => _key.currentState!.reload()),
separatorBuilder: (context, index) => const Divider(height: 1),
),
);
}
}
API reference
InfiniteScrollListView<T>
| Parameter | Type | Description |
|---|---|---|
pageLoader |
Future<List<T>?> Function(int page) |
Called with the next page index. Return null or [] to signal end of results. |
elementBuilder |
Widget Function(context, item, index, animation) |
Builds each item. Use simpleBuilder to drop unused params. |
comparator |
int Function(T a, T b)? |
Sort order. Positive = a before b, zero = same item (update), negative = a after b. |
pick |
T Function(T current, T incoming)? |
Resolves duplicates when comparator returns zero. Defaults to keeping the incoming item. |
pageSize |
int? |
Stops paginating when a page has fewer items than this. |
isEndOfPage |
bool Function(List<T> page)? |
Custom end-of-page predicate. Overrides pageSize and the default. |
initialItems |
List<T>? |
Pre-populates the list on mount without a network call. |
onReload |
void Function()? |
Fires when reload() is called explicitly (not on the initial load). |
onLoadMore |
void Function(int page)? |
Fires for every page after the first. |
errorBuilder |
Widget Function(context, error)? |
Full-page error widget shown when the list is empty and a fetch fails. |
elementErrorBuilder |
Widget Function(context, error)? |
Inline error widget shown when a subsequent page fails. |
separatorBuilder |
Widget Function(context, index)? |
Separator between items, like ListView.separated. |
removeAnimationBuilder |
Widget Function(context, item, animation)? |
Exit animation for removed items. Receives the removed item. |
betweenItemRenderDelay |
Duration? |
Delay between each item rendering. null enables the batch fast-path. |
animateRemovingItemsOnReload |
bool |
Whether to animate items out during reload(). Default false. |
refreshable |
bool |
Wraps the list in a RefreshIndicator. Default true. |
noDataWidget |
Widget? |
Shown when the list is empty and loading is done. |
loadingWidget |
Widget? |
Shown during the initial load. |
itemLoadingWidget |
Widget? |
Shown at the bottom while the next page loads. |
endOfResultWidget |
Widget? |
Shown when all pages have been loaded. |
Named constructors
| Constructor | Description |
|---|---|
InfiniteScrollListView.once(oncePageLoader: () => ...) |
Fetches exactly one page. No index argument, no end-of-page configuration needed. |
InfiniteScrollListView.horizontal(...) |
Sets scrollDirection: Axis.horizontal. |
SliverInfiniteScrollListView.once(oncePageLoader: () => ...) |
Sliver variant of .once. |
Top-level helpers
| Helper | Description |
|---|---|
simpleBuilder<T>((context, item) => ...) |
Wraps a two-argument builder into the full four-argument elementBuilder signature. |
Contribute
Found a bug? Have an idea? Open an issue or PR:
https://github.com/Oualitsen/flutter_infinite_scroll_list_view