Skip to content

Instantly share code, notes, and snippets.

@AymanProjects
Last active February 6, 2022 12:42
Show Gist options
  • Save AymanProjects/aabdb59137bc2f1391f97ff026b75ba9 to your computer and use it in GitHub Desktop.
Save AymanProjects/aabdb59137bc2f1391f97ff026b75ba9 to your computer and use it in GitHub Desktop.
Flutter_PaginatedList
import 'package:flutter/material.dart';
class PaginatedList<T> extends StatefulWidget {
/// You should pass a future that paginate itself using [lastItem].
/// And return ``List<T>`` where ``T`` is your database model.
/// An example would be something like this:
/// ```dart
/// return FirebaseFirestore.instance
/// .collection('todos')
/// .orderBy('createdAt')
/// .startAfter([lastItem?.createdAt ?? 0])
/// .limit(10)
/// .get().then((snapshot) => snapshot.docs.map((doc) => Todo.fromJson(doc.data())));
/// ```
/// [lastItem] will return null if [paginatedFuture] returned an empty list or null,
/// so that you can check if [lastItem] is null then paginate from the start. as written in the example
final Future<List<T>> Function(T lastItem) paginatedFuture;
/// [itemBuilder] will loop through all stored items, so that you can build your own widgets.
final Widget Function(T object) itemBuilder;
/// This widget will be shown when [paginatedFuture] did not bring any results at first attempt.
/// Typicaly, you would show a cool illustration.
final Widget noResultsFound;
/// This widget will be shown in tow places:
/// 1- when [paginatedFuture] is loading for the first time.
/// 2- appended at the bottom of the list when paginating.
final Widget loadingIndicator;
/// This widget will be appended at the bottom of the list when there is no more data to load.
/// Under the hood we are just checking if [paginatedFuture] return an empty list or not.
final Widget endOfResultsWidget;
/// Padding around the list
final EdgeInsets padding;
/// A space to seperate the list items
final double spaceBetween;
/// To control how the list behaive
final ScrollPhysics physics;
/// You might want to load the next page of data before the user reach the end of the list.
/// The higher the offset, the earlier [paginatedFuture] is called.
final double offsetToEndOfList;
/// A paginated list that will call [paginatedFuture] every time the user scroll to the end of the list.
/// To clear all the data and fetch from the beggining, provide a key
/// of type [PaginatedListState] to access the method [clearAndReload].
///
/// Example of using this package:
/// ```dart
/// PaginatedList<Todo>(
/// paginatedFuture: (lastItem) => paginate(lastItem),
/// itemBuilder: (todo) => Todo(todo),
/// );
///
/// Future<List<Todo>> paginate(lastItem){
/// return FirebaseFirestore.instance
/// .collection('todos')
/// .orderBy('createdAt')
/// .startAfter([lastItem?.createdAt ?? 0])
/// .limit(10)
/// .get().then((snapshot) => snapshot.docs.map((doc) => Todo.fromJson(doc.data())));
/// }
///
/// ```
PaginatedList({
@required this.paginatedFuture,
@required this.itemBuilder,
this.loadingIndicator =
const Center(child: const CircularProgressIndicator()),
this.endOfResultsWidget = const Text('End of results'),
this.padding = EdgeInsets.zero,
this.spaceBetween = 4,
this.physics = const BouncingScrollPhysics(
parent: const AlwaysScrollableScrollPhysics(),
),
this.noResultsFound =
const Center(child: const Text('No Results were found :(')),
this.offsetToEndOfList = 100,
Key key,
}) : assert(paginatedFuture != null),
assert(itemBuilder != null),
super(key: key);
@override
PaginatedListState<T> createState() => PaginatedListState<T>();
}
class PaginatedListState<T> extends State<PaginatedList<T>> {
final _scrollController = ScrollController();
List<T> _allItems;
bool _isPaginating = false;
bool _noResultsFound = false;
bool _hasMoreData = true;
@override
void initState() {
super.initState();
_fetch().then((_) => _scrollController?.addListener(_onEndOfListReached));
}
@override
void setState(fn) {
if (this.mounted) super.setState(fn);
}
void _onEndOfListReached() {
if (_scrollController.position.pixels >
_scrollController.position.maxScrollExtent - widget.offsetToEndOfList) {
if (_hasMoreData) _fetch();
}
}
Future<void> _fetch() async {
if (_isPaginating) return;
setState(() => _isPaginating = true);
_noResultsFound = false;
_hasMoreData = true;
await widget.paginatedFuture(_allItems?.last).then((list) {
if (_allItems == null) {
if (list == null || list.isEmpty) {
_noResultsFound = true;
_hasMoreData = false;
} else {
_allItems = list;
}
} else {
if (list == null || list.isEmpty) {
_hasMoreData = false;
} else {
_allItems.addAll(list);
}
}
});
setState(() => _isPaginating = false);
}
void clearAndReload() {
_allItems = null;
_fetch();
}
@override
void dispose() {
_scrollController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_noResultsFound)
return widget.noResultsFound;
else
return _allItems == null ? widget.loadingIndicator : _buildItems();
}
Widget _buildItems() {
return ListView.separated(
physics: widget.physics,
padding: widget.padding,
itemCount: _allItems.length,
separatorBuilder: (_, __) => SizedBox(height: widget.spaceBetween),
controller: _scrollController,
itemBuilder: (_, index) {
if (index == _allItems.length - 1)
return Column(
children: [
widget.itemBuilder(_allItems[index]),
const SizedBox(height: 12),
_isPaginating
? widget.loadingIndicator
: widget.endOfResultsWidget,
],
);
else
return widget.itemBuilder(_allItems[index]);
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment