Last active
February 6, 2022 12:42
-
-
Save AymanProjects/aabdb59137bc2f1391f97ff026b75ba9 to your computer and use it in GitHub Desktop.
Flutter_PaginatedList
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 '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