Created
July 7, 2021 00:12
-
-
Save stargazing-dino/4065ebc4ff159767fbce4793be17c5a2 to your computer and use it in GitHub Desktop.
a two way pagination builder for flutter Listviews
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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