Skip to content

Instantly share code, notes, and snippets.

@stargazing-dino
Created July 7, 2021 00:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stargazing-dino/4065ebc4ff159767fbce4793be17c5a2 to your computer and use it in GitHub Desktop.
Save stargazing-dino/4065ebc4ff159767fbce4793be17c5a2 to your computer and use it in GitHub Desktop.
a two way pagination builder for flutter Listviews
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension LoadingWidgets on List<Widget> {
// TODO: This will likely fail the requirements of a [ListView.builder]
/// Adds [loadingWidgetAfter] and [loadingWidgetBefore] to the list.
List<Widget> addLoadingIndicators(
Widget? loadingWidgetAfter,
Widget? loadingWidgetBefore,
) {
return [
if (loadingWidgetBefore != null) loadingWidgetBefore,
...this,
if (loadingWidgetAfter != null) loadingWidgetAfter,
];
}
}
/// A function signature for a builder that recieves loading widgets to be
/// inserted into a ListView when further data is being fetched.
typedef LazyBuilder = Widget Function(
BuildContext context,
Widget? loadingWidgetBefore,
Widget? loadingWidgetAfter,
);
class LazyListBuilder extends StatefulWidget {
/// A lazy builder that will be used to create the list widget. It also
/// carries the context of the child and a loading widget to be inserted into
/// the ListViews.
///
/// Note, the direct child of this widget should be the [ListView]. If it is
/// not and the [ListView] is further down the tree then it's likely that this
/// will Builder will fail to recognize scroll events unless [ignoreDepth] is
/// set to true.
final LazyBuilder builder;
/// Called when more content is needed at the start of the main scroll axis
final AsyncCallback? onLoadBefore;
/// Called when more content is needed at the end of the main scroll axis
final AsyncCallback onLoadAfter;
final Duration timeout;
/// The duration until either [onLoadBefore] or [onLoadAfter] is called.
/// The debounce strategy is eager. That means the callback will be called
/// first then further calls will be ignored until the timeout expires.
final Duration debounceDuration;
/// Whether to ignore the depth of the [ListView] when calling [onLoadBefore]
/// or [onLoadAfter].
final bool ignoreDepth;
const LazyListBuilder({
Key? key,
required this.builder,
required this.onLoadBefore,
required this.onLoadAfter,
this.debounceDuration = const Duration(milliseconds: 500),
this.timeout = const Duration(seconds: 10),
this.ignoreDepth = false,
}) : super(key: key);
@override
State<LazyListBuilder> createState() => _LazyListBuilderState();
}
class _LazyListBuilderState extends State<LazyListBuilder> {
bool isLoadingBefore = false;
bool isLoadingAfter = false;
Timer? timer;
Future<void> loadBefore() async {
assert(widget.onLoadBefore != null);
// If we have a timer going, just return early.
if (timer?.isActive ?? false) {
return;
} else {
setState(() => isLoadingBefore = true);
if (mounted) {
await widget.onLoadBefore?.call().timeout(widget.timeout);
setState(() => isLoadingBefore = false);
}
timer = Timer(widget.debounceDuration, () {});
}
}
Future<void> loadAfter() async {
if (timer?.isActive ?? false) {
return;
} else {
setState(() => isLoadingAfter = true);
if (mounted) {
// TODO: Will these need completers to correctly handle errors?
await widget.onLoadAfter().timeout(widget.timeout);
setState(() => isLoadingAfter = false);
}
timer = Timer(widget.debounceDuration, () {});
}
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final defaultLoadingWidget = Padding(
padding: EdgeInsets.symmetric(vertical: theme.visualDensity.vertical),
child: const Center(child: CircularProgressIndicator()),
);
return NotificationListener<ScrollNotification>(
child: widget.builder(
context,
isLoadingBefore ? defaultLoadingWidget : null,
isLoadingAfter ? defaultLoadingWidget : null,
),
onNotification: (notification) {
// Note, this will listen to only immediate scroll events.
if (notification.depth == 0 || widget.ignoreDepth) {
if (notification is ScrollEndNotification) {
// Only if we're dragging in that direction will we try to load in
// more items.
final primaryVelocity = notification.dragDetails?.primaryVelocity;
if (widget.onLoadBefore != null) {
if (primaryVelocity != null &&
primaryVelocity < 0 &&
notification.metrics.extentBefore == 0) {
loadBefore();
}
}
if (primaryVelocity != null &&
primaryVelocity > 0 &&
notification.metrics.extentAfter == 0) {
loadAfter();
}
}
}
return false;
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment