mo_infinite_scroll 1.0.2
mo_infinite_scroll: ^1.0.2 copied to clipboard
Infinite scroll for Flutter with pull-to-refresh, pre-fetch, a Sliver variant, and customisable loading, empty, and error placeholders.
// ignore_for_file: public_member_api_docs
/// This file is the pub.dev example tab for mo_infinite_scroll.
///
/// It shows the two most common use cases side by side:
/// 1. [MoInfiniteScrollFlow] – standalone list with built-in pull-to-refresh.
/// 2. [MoSliverInfiniteScrollFlow] – 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';
import 'package:mo_infinite_scroll/mo_infinite_scroll_controller.dart';
import 'package:mo_infinite_scroll/mo_infinite_scroll_sliver.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: [
// ── Option 1: standalone list ──────────────────────────────────
FilledButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _StandalonePage()),
),
child: const Text('InfiniteScrollFlow (standalone)'),
),
const SizedBox(height: 12),
// ── Option 2: sliver inside CustomScrollView ───────────────────
FilledButton.tonal(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _SliverPage()),
),
child: const Text('MoSliverInfiniteScrollFlow'),
),
],
),
),
);
}
}
// ── Example 1 — InfiniteScrollFlow ───────────────────────────────────────────
//
// 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('Standalone'),
actions: [
// Refresh from outside the widget via the external controller.
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
onPressed: () => _controller.refresh(_fetchPosts, 10),
),
],
),
body: MoInfiniteScroll<Post>(
controller: _controller,
fetcher: _fetchPosts,
limit: 10,
prefetchOffset: 3,
itemBuilder: (context, post) => _PostTile(post: post),
separatorBuilder: (_, __) => const Divider(height: 1),
),
);
}
}
// ── Example 2 — MoSliverInfiniteScrollFlow ─────────────────────────────────────
//
// 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(_fetchPosts, 10),
child: CustomScrollView(
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}'),
);
}
}