lemmur-app-android/lib/widgets/infinite_scroll.dart

114 lines
3.1 KiB
Dart
Raw Normal View History

2020-09-17 19:43:26 +02:00
import 'package:flutter/material.dart';
2021-01-09 01:35:43 +01:00
import 'package:flutter/services.dart';
2020-09-17 19:43:26 +02:00
import 'package:flutter_hooks/flutter_hooks.dart';
import '../hooks/ref.dart';
class InfiniteScrollController {
2021-01-03 21:03:32 +01:00
VoidCallback clear;
InfiniteScrollController() {
usedBeforeCreation() => throw Exception(
'Tried to use $runtimeType before it being initialized');
clear = usedBeforeCreation;
}
void dispose() {
clear = null;
}
}
2020-09-30 19:05:00 +02:00
/// `ListView.builder` with asynchronous data fetching
2020-09-17 19:43:26 +02:00
class InfiniteScroll<T> extends HookWidget {
final int batchSize;
final Widget loadingWidget;
final Widget Function(T data) builder;
final Future<List<T>> Function(int page, int batchSize) fetchMore;
final InfiniteScrollController controller;
final Widget prepend;
final EdgeInsetsGeometry padding;
2021-01-09 01:35:43 +01:00
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
2020-09-17 19:43:26 +02:00
2021-01-09 01:35:43 +01:00
InfiniteScroll({
2020-09-17 19:43:26 +02:00
this.batchSize = 10,
this.prepend = const SizedBox.shrink(),
this.padding,
2020-09-17 19:43:26 +02:00
this.loadingWidget =
const ListTile(title: Center(child: CircularProgressIndicator())),
2020-09-18 23:29:38 +02:00
@required this.builder,
@required this.fetchMore,
this.controller,
2020-09-17 19:43:26 +02:00
}) : assert(builder != null),
assert(fetchMore != null),
assert(batchSize > 0);
2020-09-17 19:43:26 +02:00
@override
Widget build(BuildContext context) {
final data = useState<List<T>>([]);
final hasMore = useRef(true);
final isFetching = useRef(false);
2020-09-17 19:43:26 +02:00
useEffect(() {
if (controller != null) {
controller.clear = () {
data.value = [];
hasMore.current = true;
};
return controller.dispose;
}
return null;
}, []);
final page = data.value.length ~/ batchSize + 1;
2021-01-09 01:35:43 +01:00
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () async {
controller.clear();
await HapticFeedback.mediumImpact();
await Future.delayed(const Duration(seconds: 1));
},
child: ListView.builder(
padding: padding,
// +2 for the loading widget and prepend widget
itemCount: data.value.length + 2,
itemBuilder: (_, i) {
if (i == 0) {
return prepend;
2020-09-17 19:43:26 +02:00
}
2021-01-09 01:35:43 +01:00
i -= 1;
// reached the bottom, fetch more
if (i == data.value.length) {
// if there are no more, skip
if (!hasMore.current) {
return const SizedBox.shrink();
}
// if it's already fetching more, skip
if (!isFetching.current) {
isFetching.current = true;
fetchMore(page, batchSize).then((newData) {
// if got less than the batchSize, mark the list as done
if (newData.length < batchSize) {
hasMore.current = false;
}
// append new data
data.value = [...data.value, ...newData];
}).whenComplete(() => isFetching.current = false);
}
return loadingWidget;
}
2020-09-17 19:43:26 +02:00
2021-01-09 01:35:43 +01:00
// not last element, render list item
return builder(data.value[i]);
},
),
2020-09-17 19:43:26 +02:00
);
}
}