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.
// ignore_for_file: public_member_api_docs
/// This file is the pub.dev example tab for mo_infinite_scroll.
///
/// It shows the three most common use cases:
/// 1. [MoInfiniteScroll] – standalone vertical list with built-in
/// pull-to-refresh.
/// 2. [MoInfiniteScroll] – horizontal list (carousel style).
/// 3. [MoInfiniteScrollSliver] – sliver variant inside a [CustomScrollView],
/// with pull-to-refresh delegated to the parent [RefreshIndicator].
library;
import 'package:flutter/material.dart';
import 'package:mo_infinite_scroll/mo_infinite_scroll.dart';
void main() => runApp(const App());
// ── Fake model & API ──────────────────────────────────────────────────────────
class Post {
const Post({required this.id, required this.title});
final int id;
final String title;
}
/// Simulates a remote API: 47 posts total, 800 ms latency.
Future<List<Post>> _fetchPosts(int page, int limit) async {
await Future.delayed(const Duration(milliseconds: 800));
const total = 47;
final start = (page - 1) * limit;
if (start >= total) return [];
return List.generate(
(start + limit).clamp(0, total) - start,
(i) => Post(id: start + i + 1, title: 'Post #${start + i + 1}'),
);
}
// ── App shell ─────────────────────────────────────────────────────────────────
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'mo_infinite_scroll example',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const _HomePage(),
);
}
}
class _HomePage extends StatelessWidget {
const _HomePage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('mo_infinite_scroll')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _StandalonePage()),
),
child: const Text('MoInfiniteScroll (vertical)'),
),
const SizedBox(height: 12),
FilledButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _HorizontalPage()),
),
child: const Text('MoInfiniteScroll (horizontal)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _SliverPage()),
),
child: const Text('MoInfiniteScrollSliver'),
),
],
),
),
);
}
}
// ── Example 1 — MoInfiniteScroll (vertical) ──────────────────────────────────
//
// The simplest usage: only [fetcher] and [itemBuilder] are required.
// Pull-to-refresh is built in. An external [MoInfiniteScrollController] is
// passed so the AppBar action can also trigger a refresh.
class _StandalonePage extends StatefulWidget {
const _StandalonePage();
@override
State<_StandalonePage> createState() => _StandalonePageState();
}
class _StandalonePageState extends State<_StandalonePage> {
final _controller = MoInfiniteScrollController<Post>();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Vertical'),
actions: [
// Refresh from outside the widget via the external controller.
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
onPressed: _controller.refresh,
),
],
),
body: MoInfiniteScroll<Post>(
controller: _controller,
fetcher: _fetchPosts,
limit: 10,
prefetchOffset: 3,
itemBuilder: (context, post) => _PostTile(post: post),
separatorBuilder: (_, __) => const Divider(height: 1),
),
);
}
}
// ── Example 2 — MoInfiniteScroll (horizontal) ────────────────────────────────
//
// Set [scrollDirection] to Axis.horizontal for carousel-style lists.
// Pull-to-refresh is automatically disabled on the horizontal axis.
class _HorizontalPage extends StatelessWidget {
const _HorizontalPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Horizontal')),
body: Center(
child: SizedBox(
height: 160,
child: MoInfiniteScroll<Post>(
fetcher: _fetchPosts,
limit: 10,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemBuilder: (context, post) => Card(
child: SizedBox(
width: 140,
child: Center(child: Text(post.title)),
),
),
),
),
),
);
}
}
// ── Example 3 — MoInfiniteScrollSliver ───────────────────────────────────────
//
// Drop the sliver into any CustomScrollView. Pull-to-refresh is handled by
// the wrapping RefreshIndicator; the controller is passed in so it can be
// called from onRefresh.
class _SliverPage extends StatefulWidget {
const _SliverPage();
@override
State<_SliverPage> createState() => _SliverPageState();
}
class _SliverPageState extends State<_SliverPage> {
final _controller = MoInfiniteScrollController<Post>();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: _controller.refresh,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
const SliverAppBar(
title: Text('Sliver'),
floating: true,
snap: true,
),
MoInfiniteScrollSliver<Post>(
controller: _controller,
fetcher: _fetchPosts,
limit: 10,
prefetchOffset: 3,
itemBuilder: (context, post) => _PostTile(post: post),
separatorBuilder: (_, __) =>
const Divider(height: 1, indent: 16, endIndent: 16),
),
],
),
),
);
}
}
// ── Shared item tile ──────────────────────────────────────────────────────────
class _PostTile extends StatelessWidget {
const _PostTile({required this.post});
final Post post;
@override
Widget build(BuildContext context) {
return ListTile(
leading: CircleAvatar(child: Text('${post.id}')),
title: Text(post.title),
subtitle: Text('Page ${((post.id - 1) ~/ 10) + 1}'),
);
}
}