Pagination with streams in flutter - flutter

I have implemented a pagination scheme in my flutter app but im not sure if its performance friendly and if it may result to trouble on production in future so i would like to get advice on it.
here is my implementation
First, i get data using a stream provider in my parent widget.
class BuyerSellerPostsPage extends StatefulWidget {
#override
_BuyerSellerPostsPageState createState() => _BuyerSellerPostsPageState();
}
class _BuyerSellerPostsPageState extends State<BuyerSellerPostsPage> {
...some code here...
bool isAtBottom = false;
int postToDisplay=10;
#override
void initState() {
super.initState();
// Setup the listener.
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
setState(() {
isAtBottom=true;
postToDisplay+=10;
print('now true');
});
}else{
setState(() {
print('now false');
isAtBottom=false;
});
}
}
});
}
#override
void dispose(){
_scrollController.dispose();
super.dispose();
}
...some code here..
#override
Widget build(BuildContext context) {
Position coordinates=Provider.of<Position>(context);
...some code here...
body: SingleChildScrollView(
controller: _scrollController,
child: StreamProvider<List<SellerPost>>.value(
value: SellerDatabaseService(
currentLocation: new GeoPoint(coordinates.latitude, coordinates.longitude,)
,filters: _setFilters,
selectedCategory: _selectedCategory,
selectedTag: _selectedTag,
postsToDisplay: postToDisplay
).inRangeSellerPosts ,
...some code here...
);
}else{
return Container(
);
}
},
),
),
//post list
BSellerPostList(),
...some code here...
}
}
The initial posts to display are 10.
In my initstate i have used a listener to my scroll controller so that when the user scrolls to the bottom more items(+10) are loaded on screen.
In my stream provider i pass the postsToDisplay int to my stream in the backend below
Stream <List<SellerPost>> get inRangeSellerPosts {
try {
return sellerPostCollection
.where("expireTime" , isGreaterThan: DateTime.now())
.orderBy('expireTime',descending: true)
.snapshots()
.map(yieldSellerPosts);
} catch (e) {
print(e.toString());
return null;
}
}
List<SellerPost> yieldSellerPosts(QuerySnapshot snapshot) {
List<String> l = [];
print(snapshot.documents.length);
try {
return snapshot.documents.map((doc) {
return SellerPost(
...some code here...
);
}).take(postsToDisplay)
.toList();
} catch (e) {
print(e.toString());
return null;
}
}
I now get the snapshots and use the take method on the list to get only the required number(postsToDisplay).
This method works fine in my debug mode. Im not sure how it will behave in production or with large data sets. Could someone scrutinise it, i would appreciate alot.

I personally used a modified version of this answer posted in a previous similar question.
Both my implementation and that other guys implementation have the pros and cons.
His method leads to listening to document changes for documents you may never need to use considering you may need only 10 paginated items. My method on the other hand does not work along with the stream. It uses a future to query for document snapshots to update the list as you proceed.
Here is the sample code
bool _isRequesting = false;
bool _isFinish = false;
final _scrollController = ScrollController();
List<DocumentSnapshot> _posts = [];
#override
void initState() {
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
setState(() {
requestNextPage();
});
}
}
});
requestNextPage();
super.initState();
}
void requestNextPage() async {
try{
if (!_isRequesting && !_isFinish) {
QuerySnapshot querySnapshot;
_isRequesting = true;
if (_posts.isEmpty) {
//check if _posts list is empty so that we may render the first list of 10 items.
querySnapshot = await Firestore.instance
.collection('sellerPost')
.limit(10)
.getDocuments();
} else {
//if _posts list is not empty it means we have already rendered the first 10 items so we start querying from where we left off to avoid repetition.
querySnapshot = await Firestore.instance
.collection('sellerPost')
.startAfterDocument(_posts[_posts.length - 1])
.limit(10)
.getDocuments();
}
if (querySnapshot != null) {
int oldSize = _posts.length;
_posts.addAll(querySnapshot.documents);
int newSize = _posts.length;
if (oldSize == newSize) {
_isFinish = true;
}
_isRequesting = false;
}else{
_isFinish = false;
_isRequesting = false;
}
}catch(e){
print(e.toString());
}
}
So in this above code i used the scroll controller to detect when the user scrolls to the bottom of the page with the paginated items e.g 10 posts. This event triggers my function requestNextPage(); Note that on inititState we also call the requestNextPage(); to render the initial 10 posts.
So now, the each time a scroll to bottom is detected, 10 extra posts are added to
_posts

Related

Pagination for Flutter ListView.builder [duplicate]

I'm trying to paginate by using Firestore and I read the document and it implement like this in Swift
let first = db.collection("cities")
.order(by: "population")
.limit(to: 25)
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
// Construct a new query starting after this document,
// retrieving the next 25 cities.
let next = db.collection("cities")
.order(by: "population")
.start(afterDocument: lastSnapshot)
// Use the query for pagination.
// ...
}
Just for practice, I tried fetched three documents and if button tapped, fetch one more document.
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').limit(3).getDocuments().then((snapshot) {
_lastDocument = snapshot.documents.last;
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
After button tapped tried like this.
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').startAfter(_lastDocument).limit(1).getDocuments().then((snapshot) {
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
But console says this.
The following assertion was thrown while handling a gesture: type
'DocumentSnapshot' is not a subtype of type 'List[dynamic]'
Why do I have to pass list?
Does anyone know how to fix this?
UPDATE
I was able to paginate like so.
class PaginationExample extends StatefulWidget {
#override
_PaginationExampleState createState() => _PaginationExampleState();
}
class _PaginationExampleState extends State<PaginationExample> {
var _restaurants = <Restaurant>[];
var _nomore = false;
var _isFetching = false;
DocumentSnapshot _lastDocument;
ScrollController _controller;
void _fetchDocuments() async {
final QuerySnapshot querySnapshot = await Firestore.instance.collection('restaurants').orderBy('likes').limit(8).getDocuments();
// your logic here
}
Future<Null> _fetchFromLast() async {
final QuerySnapshot querySnapshot = await Firestore.instance.collection('restaurants').orderBy('likes').startAfter([_lastDocument['likes']]).limit(4).getDocuments();
if (querySnapshot.documents.length < 4) {
_nomore = true;
return;
}
_lastDocument = querySnapshot.documents.last;
for (final DocumentSnapshot snapshot in querySnapshot.documents) {
final Restaurant re = Restaurant(snapshot);
_restaurants.add(re);
}
setState(() {});
}
void _scrollListener() async {
if (_nomore) return;
if (_controller.position.pixels == _controller.position.maxScrollExtent && _isFetching == false) {
_isFetching = true;
await _fetchFromLast();
_isFetching = false;
}
}
#override
void initState() {
_fetchDocuments();
_controller = new ScrollController()..addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
);
}
}
There is an error here:
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').startAfter(_lastDocument).limit(1).getDocuments().then((snapshot) {
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
startAfter method expects a List value params and you are passing a DocumentSnapshot.
Takes a list of [values], creates and returns a new [Query] that
starts after the provided fields relative to the order of the query.
You could try something like this:
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').startAfter([{'name': 'Tom'}]).limit(1).getDocuments().then((snapshot) {
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
Paginate just with 2 attrubutes, itemBuilder and query using this package - paginate_firestore
For example,
PaginateFirestore(
itemBuilder: (context, documentSnapshot) => ListTile(
leading: CircleAvatar(child: Icon(Icons.person)),
title: Text(documentSnapshot.data['name']),
subtitle: Text(documentSnapshot.documentID),
),
// orderBy is compulsary to enable pagination
query: Firestore.instance.collection('users').orderBy('name'),
)
This works for me giving realtime pagination
defining functions to fetch data
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../core/constants/firebase_constants.dart';
class FirebaseProvider {
final FirebaseFirestore _firestore;
FirebaseProvider({required FirebaseFirestore firestore})
: _firestore = firestore;
CollectionReference get _posts =>
_firestore.collection(FirebaseConstants.postsCollection);
Future<List<DocumentSnapshot>> fetchFirstList(
String fromgst, String postType) async {
return (await _posts
.where("fromgst", isEqualTo: fromgst)
.where("postType", isEqualTo: postType)
.orderBy("date", descending: true)
.limit(5)
.get())
.docs;
}
Future<List<DocumentSnapshot>> fetchNextList(String fromgst, String postType,
List<DocumentSnapshot> documentList) async {
return (await _posts
.where("fromgst", isEqualTo: fromgst)
.where("postType", isEqualTo: postType)
.orderBy("date", descending: true)
.startAfterDocument(documentList[documentList.length - 1])
.limit(5)
.get())
.docs;
}
}
separate class to handle pagination
import 'dart:async';
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:growmore/features/home/repository/firebase_provider.dart';
import 'package:rxdart/rxdart.dart';
class PostListBloc {
List<DocumentSnapshot>? documentList;
bool showIndicator = false;
FirebaseProvider? firebaseProvider;
BehaviorSubject<List<DocumentSnapshot>>? postController;
BehaviorSubject<bool>? showIndicatorController;
PostListBloc() {
postController = BehaviorSubject<List<DocumentSnapshot>>();
showIndicatorController = BehaviorSubject<bool>();
firebaseProvider = FirebaseProvider(firestore: FirebaseFirestore.instance);
}
Stream get getShowIndicatorStream => showIndicatorController!.stream;
Stream<List<DocumentSnapshot>> get postStream => postController!.stream;
// This method will automatically fetch first 10 elements from the document list
Future fetchFirstList(String fromgst, String postType) async {
try {
documentList = await firebaseProvider?.fetchFirstList(fromgst, postType);
print("documentList$documentList");
postController?.sink.add(documentList!);
try {
if (documentList!.isEmpty) {
postController?.sink.addError("No Data Available");
}
} catch (e) {
print(e);
}
} on SocketException {
postController?.sink.addError(SocketException("No Internet Connection"));
} catch (e) {
print(e.toString());
postController?.sink.addError(e);
}
}
//This will automatically fetch the next 10 elements from the list
fetchNextPosts(String fromgst, String postType) async {
try {
updateIndicator(true);
List<DocumentSnapshot> newDocumentList = await firebaseProvider!
.fetchNextList(fromgst, postType, documentList!);
print('asca$newDocumentList');
documentList!.addAll(newDocumentList);
postController!.sink.add(documentList!);
try {
if (documentList!.isEmpty) {
postController!.sink.addError("No Data Available");
updateIndicator(false);
}
} catch (e) {
updateIndicator(false);
}
} on SocketException {
postController!.sink.addError(SocketException("No Internet Connection"));
updateIndicator(false);
} catch (e) {
updateIndicator(false);
print(e.toString());
postController!.sink.addError(e);
}
}
//For updating the indicator below every list and paginate*
updateIndicator(bool value) async {
showIndicator = value;
showIndicatorController!.sink.add(value);
}
void dispose() {
postController!.close();
showIndicatorController!.close();
}
}
the ui part
ScrollController controller = ScrollController();
#override
void initState() {
super.initState();
postListBloc = PostListBloc();
print("dvvfe${widget.fromgst}");
postListBloc!.fetchFirstList(widget.fromgst, widget.postType);
controller.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<List<DocumentSnapshot>>(
stream: postListBloc!.postStream,
builder: (context, snapshot) {
if (snapshot.data != null) {
return ListView.builder(
itemCount: snapshot.data?.length,
shrinkWrap: true,
controller: controller,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(snapshot.data![index]['description']),
),
),
);
},
);
} else {
return const CircularProgressIndicator();
}
},
),
);
}
void _scrollListener() {
if (controller.offset >= controller.position.maxScrollExtent &&
!controller.position.outOfRange) {
print("Cavc$controller");
print("at the end of list");
postListBloc!.fetchNextPosts(widget.fromgst, widget.postType);
}
}
}
I found it not open source github repo

Flutter - How to complete/await function execution before build method?

I am simply trying to set an ID in this function:
_getLastWorkoutId() async {
try {
var snapshot = await usersRef
.doc(currentUser!.uid)
.collection('workouts')
.orderBy('workoutDate', descending: true)
.limit(1)
.snapshots()
.first;
//The execution moves to build method from here------and then returns
for (var element in snapshot.docs) {
workoutId = element.id;
setState(() {
_isWorkoutIdSet = true;
});
}
//return snapshot;
} catch (e) {
print(e.toString());
}
//return null;
}
I call it in the initState:
#override
void initState() {
WidgetsBinding.instance!.addObserver(this);
super.initState();
//var snapshot = _getLastWorkoutId();
_getLastWorkoutId();
}
The problem is, the for loop executes after the build function is called. I don't want that to happen.
You can use FutureBuilder like this:
Future<bool> _value;
#override
void initState() {
WidgetsBinding.instance!.addObserver(this);
super.initState();
_value = _getLastWorkoutId();
}
And in your build method you have:
FutureBuilder<bool>(
future: _value,
builder: (
BuildContext context,
AsyncSnapshot<bool> snapshot,
) {
if (snapshot.hasData) {
if (snapshot.data){
//update view
}else{
//update view
}
}
}
The method can be like this:
_getLastWorkoutId() async {
try {
var snapshot = await usersRef
.doc(currentUser!.uid)
.collection('workouts')
.orderBy('workoutDate', descending: true)
.limit(1)
.snapshots()
.first;
for (var element in snapshot.docs) {
workoutId = element.id;
return true;
}
} catch (e) {
print(e.toString());
}
}
Here you can find more about FutureBuilder.
I believe this should solve the issue:
First, on build method:
return FutureBuilder(
future: _getLastWorkoutId(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) return CircularProgressIndicator();
return Container(); // here goes whatever it is you had before.
}
);
then on _getLastWorkoutId():
Future<void> _getLastWorkoutId() async {
...
}
That way the function returns a future of void instead of void, allowing FutureBuilder to do its thing.
You should declare a variable to save state. For example
var isLoading = true;
At the end of try block in func _getLastWorkoutId reset isLoading to false.
Then call setState or update state by better way used state management likes provider, bloc, get.
In build widget add check isLoading like this
return isLoading ? YourLoadingIndicatorWidget() : DisplayDataWidget();

Flutter: How to Access Data & NotifyListeners Outside a Stream

I have a list of items and a stream within a class. The stream triggers a future where then notifylisteners is called to update the list of items. It works, but it only shows updates within the stream. How do I notifylistners outside the stream as well?
Where, if I were to call Provider.of(context).items it won't return as empty.
Here is the following code structure.
class Mans with ChangeNotifier {
List<Man> _items = [];
List<Man> get items {
return [..._items];
}
Stream<List<Man>> stream;
bool hasMore;
bool _isLoading;
List<Man> _data;
StreamController<List<Man>> _controller;
Mans({page = 1}) {
_data = List<Man>();
_controller = StreamController<List<Man>>.broadcast();
_isLoading = false;
// Test if list prints #1
items.forEach((list) {
print("nono: ${list.id}");
});
stream = _controller.stream.map((List<Man> mansData) {
// Test if list prints #2
items.forEach((list) {
print("nono: ${list.id}");
});
return mansData;
});
// Test if list prints #3
items.forEach((list) {
print("nono2: ${list.id}");
});
hasMore = true;
refresh();
}
Future<void> refresh() {
return loadMore(
page: 1,
clearCachedData: true,
);
}
Future<void> loadMore(
{bool clearCachedData = false,
page = 1}) async {
if (clearCachedData) {
_data = List<Man>();
hasMore = true;
}
if (_isLoading || !hasMore) {
return Future.value();
}
_isLoading = true;
return await fetchAndSetMans(page)
.then((mansData) {
_isLoading = false;
_data.addAll(mansData);
hasMore = (mansData.isNotEmpty);
_controller.add(_data);
});
}
Future<List<Man>> fetchAndSetMans(page) async {
var cookie = '';
try {
print('Called_API_Mans');
var response = await SiteApi(serverConfig["url"]).getAsync("api_link?view_id=$page");
List<Man> list = [];
for (var item in response) {
//This just adds an instance of Man to the list from a Model not added to this Stack Question. It works.
list.add(Man.fromJson(item));
}
_items = list;
notifyListeners();
return _items;
} catch (error) {
return [];
}
}
As you can see, I placed three different instances where I can print the items after notifyListeners() is called in the Future 'fetchAndSetMans'.
Unfortunately, only in the one where the comment says "Test if list prints #2" does it show that the list has been updated. Basically, within the stream data.
#1 and #3 are empty.
So, anything outside of the stream, notifyListeners() doesn't update the items list.
I wish to know how I can update the value outside the stream when the future is called.
So, if I call a Provider.... like, Provider.of(context).items... I can actually get results.
Thanks, I'd appreciate any help.

Correct way to call an api by provider in fflutter?

I have been trying to make a app in flutter where an api is called and data is updated in TextField
Used provider for state management, here is the code for it.
class ProfileProvider with ChangeNotifier {
var profileData;
String _url = "http://10.0.2.2:3000/api/v1/user/loggedin_user";
void getData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.getString('token');
var data = await http.get(
_url,
headers: {
"accept": "application/json",
"content-type": "application/json",
'Token': token,
},
);
var infoOfPerson = json.decode(data.body);
profileData = new ProfileObject(
name: infoOfPerson['name'],
mobile: infoOfPerson['mobile'],
email: infoOfPerson['email'],
role: infoOfPerson['role'],
);
notifyListeners();
}
ProfileObject get profileInfo {
return profileData;
}
}
I am getting the data fine, now i have to show it in the UI, but sometime data is populated, sometime its not. Can someone please point me the right direction why this is happening.
Here is the code for UI.
class Profile extends StatefulWidget {
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
final emailController = TextEditingController(text: '');
final nameController = TextEditingController(text: '');
final mobileController = TextEditingController(text: '');
var _isInit = true;
#override
void didChangeDependencies() {
if (_isInit) {
final profileData = Provider.of<ProfileProvider>(context);
profileData.getData();
if (profileData.profileInfo != null) {
emailController.text = profileData.profileInfo.name;
nameController.text = profileData.profileInfo.email;
mobileController.text = profileData.profileInfo.mobile;
}
_isInit = false;
super.didChangeDependencies();
}
}
#override
Widget build(BuildContext context) {
final profileData = Provider.of<ProfileProvider>(context);
return Scaffold(
drawer: NavigationDrawer(),
body: profileData.profileInfo == null
? Center(
child: CircularProgressIndicator(),
)
: Builder(
builder: (context) => SingleChildScrollView(
child: Padding(.....
Below the padding, there is normal TextField, can someone tell me why the data is being populated sometime and sometime its coming empty, even I wrapped it with CircularProgressIndicator() and a check the notifyListeners(); is not working there. The loader is not being shown and data is not being loaded.
Thanks
for StatelessWidget.
Inside the build method use:
Future.microtask(() async {
context.read<SomeProvider>().fetchSomething();
});
For StatefulWidgets if you want to call it once. Do this inside the initState() or didChangeDependencies (better if the latter). This will be called at the end of the frame which means after the build or rendering finishes..
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<SomeProvider>().fetchSomething();
});
}
EDIT: WidgetsBinding will also work on build. I forgot on why I used microtask lol
i've created a function which called nextTick, i call it in initState and it works for now, but want to see others method
void nextTick(Function callback, [int milliseconds = 0]) {
Future.delayed(Duration(milliseconds: 0)).then((_) {
callback();
});
}
then use it like below
#override
void initState() {
super.initState();
nextTick((){
ProfileProvider profileProvider = Provider.of<ProfileProvider>(context);
profileProvider.getProfile();
});
}
Edit: i store couple of variables to manage them on ui, like isLoading, hasError and errorMessage. Here is my provider class
class ProfileProvider extends ChangeNotifier {
bool _hasError = false;
bool _isLoading = true;
String _errorMsg = '';
Profile _profileResponse;
bool get hasError => _hasError;
bool get isLoading => _isLoading;
String get errorMsg => _errorMsg;
Profile get profileResponse => _profileResponse;
Future<void> getProfile() async {
this.setLoading = true;
this.setError = false;
this.setErrorMsg = '';
try {
await dio.post('$api/p/view', data: {}).then((res) {
print(res.data);
_profileResponse = Profile.fromJson(jsonDecode(res.data));
print(_profileResponse.data);
notifyListeners();
}).whenComplete(() {
this.setLoading = false;
});
} catch (e) {
this.setError = true;
this.setErrorMsg = '$e';
}
this.setLoading = false;
}
set setError(bool val) {
if (val != _hasError) {
_hasError = val;
notifyListeners();
}
}
set setErrorMsg(String val) {
if (val != null && val != '') {
_errorMsg = val;
notifyListeners();
}
}
set setLoading(bool val) {
_isLoading = val;
notifyListeners();
}
}

Flutter Firestore pagination

I'm trying to paginate by using Firestore and I read the document and it implement like this in Swift
let first = db.collection("cities")
.order(by: "population")
.limit(to: 25)
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
// Construct a new query starting after this document,
// retrieving the next 25 cities.
let next = db.collection("cities")
.order(by: "population")
.start(afterDocument: lastSnapshot)
// Use the query for pagination.
// ...
}
Just for practice, I tried fetched three documents and if button tapped, fetch one more document.
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').limit(3).getDocuments().then((snapshot) {
_lastDocument = snapshot.documents.last;
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
After button tapped tried like this.
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').startAfter(_lastDocument).limit(1).getDocuments().then((snapshot) {
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
But console says this.
The following assertion was thrown while handling a gesture: type
'DocumentSnapshot' is not a subtype of type 'List[dynamic]'
Why do I have to pass list?
Does anyone know how to fix this?
UPDATE
I was able to paginate like so.
class PaginationExample extends StatefulWidget {
#override
_PaginationExampleState createState() => _PaginationExampleState();
}
class _PaginationExampleState extends State<PaginationExample> {
var _restaurants = <Restaurant>[];
var _nomore = false;
var _isFetching = false;
DocumentSnapshot _lastDocument;
ScrollController _controller;
void _fetchDocuments() async {
final QuerySnapshot querySnapshot = await Firestore.instance.collection('restaurants').orderBy('likes').limit(8).getDocuments();
// your logic here
}
Future<Null> _fetchFromLast() async {
final QuerySnapshot querySnapshot = await Firestore.instance.collection('restaurants').orderBy('likes').startAfter([_lastDocument['likes']]).limit(4).getDocuments();
if (querySnapshot.documents.length < 4) {
_nomore = true;
return;
}
_lastDocument = querySnapshot.documents.last;
for (final DocumentSnapshot snapshot in querySnapshot.documents) {
final Restaurant re = Restaurant(snapshot);
_restaurants.add(re);
}
setState(() {});
}
void _scrollListener() async {
if (_nomore) return;
if (_controller.position.pixels == _controller.position.maxScrollExtent && _isFetching == false) {
_isFetching = true;
await _fetchFromLast();
_isFetching = false;
}
}
#override
void initState() {
_fetchDocuments();
_controller = new ScrollController()..addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
);
}
}
There is an error here:
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').startAfter(_lastDocument).limit(1).getDocuments().then((snapshot) {
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
startAfter method expects a List value params and you are passing a DocumentSnapshot.
Takes a list of [values], creates and returns a new [Query] that
starts after the provided fields relative to the order of the query.
You could try something like this:
Firestore.instance.collection('user').where('name', isEqualTo: 'Tom').orderBy('age').startAfter([{'name': 'Tom'}]).limit(1).getDocuments().then((snapshot) {
snapshot.documents.forEach((snap) {
print(snap.data);
});
});
Paginate just with 2 attrubutes, itemBuilder and query using this package - paginate_firestore
For example,
PaginateFirestore(
itemBuilder: (context, documentSnapshot) => ListTile(
leading: CircleAvatar(child: Icon(Icons.person)),
title: Text(documentSnapshot.data['name']),
subtitle: Text(documentSnapshot.documentID),
),
// orderBy is compulsary to enable pagination
query: Firestore.instance.collection('users').orderBy('name'),
)
This works for me giving realtime pagination
defining functions to fetch data
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../core/constants/firebase_constants.dart';
class FirebaseProvider {
final FirebaseFirestore _firestore;
FirebaseProvider({required FirebaseFirestore firestore})
: _firestore = firestore;
CollectionReference get _posts =>
_firestore.collection(FirebaseConstants.postsCollection);
Future<List<DocumentSnapshot>> fetchFirstList(
String fromgst, String postType) async {
return (await _posts
.where("fromgst", isEqualTo: fromgst)
.where("postType", isEqualTo: postType)
.orderBy("date", descending: true)
.limit(5)
.get())
.docs;
}
Future<List<DocumentSnapshot>> fetchNextList(String fromgst, String postType,
List<DocumentSnapshot> documentList) async {
return (await _posts
.where("fromgst", isEqualTo: fromgst)
.where("postType", isEqualTo: postType)
.orderBy("date", descending: true)
.startAfterDocument(documentList[documentList.length - 1])
.limit(5)
.get())
.docs;
}
}
separate class to handle pagination
import 'dart:async';
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:growmore/features/home/repository/firebase_provider.dart';
import 'package:rxdart/rxdart.dart';
class PostListBloc {
List<DocumentSnapshot>? documentList;
bool showIndicator = false;
FirebaseProvider? firebaseProvider;
BehaviorSubject<List<DocumentSnapshot>>? postController;
BehaviorSubject<bool>? showIndicatorController;
PostListBloc() {
postController = BehaviorSubject<List<DocumentSnapshot>>();
showIndicatorController = BehaviorSubject<bool>();
firebaseProvider = FirebaseProvider(firestore: FirebaseFirestore.instance);
}
Stream get getShowIndicatorStream => showIndicatorController!.stream;
Stream<List<DocumentSnapshot>> get postStream => postController!.stream;
// This method will automatically fetch first 10 elements from the document list
Future fetchFirstList(String fromgst, String postType) async {
try {
documentList = await firebaseProvider?.fetchFirstList(fromgst, postType);
print("documentList$documentList");
postController?.sink.add(documentList!);
try {
if (documentList!.isEmpty) {
postController?.sink.addError("No Data Available");
}
} catch (e) {
print(e);
}
} on SocketException {
postController?.sink.addError(SocketException("No Internet Connection"));
} catch (e) {
print(e.toString());
postController?.sink.addError(e);
}
}
//This will automatically fetch the next 10 elements from the list
fetchNextPosts(String fromgst, String postType) async {
try {
updateIndicator(true);
List<DocumentSnapshot> newDocumentList = await firebaseProvider!
.fetchNextList(fromgst, postType, documentList!);
print('asca$newDocumentList');
documentList!.addAll(newDocumentList);
postController!.sink.add(documentList!);
try {
if (documentList!.isEmpty) {
postController!.sink.addError("No Data Available");
updateIndicator(false);
}
} catch (e) {
updateIndicator(false);
}
} on SocketException {
postController!.sink.addError(SocketException("No Internet Connection"));
updateIndicator(false);
} catch (e) {
updateIndicator(false);
print(e.toString());
postController!.sink.addError(e);
}
}
//For updating the indicator below every list and paginate*
updateIndicator(bool value) async {
showIndicator = value;
showIndicatorController!.sink.add(value);
}
void dispose() {
postController!.close();
showIndicatorController!.close();
}
}
the ui part
ScrollController controller = ScrollController();
#override
void initState() {
super.initState();
postListBloc = PostListBloc();
print("dvvfe${widget.fromgst}");
postListBloc!.fetchFirstList(widget.fromgst, widget.postType);
controller.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<List<DocumentSnapshot>>(
stream: postListBloc!.postStream,
builder: (context, snapshot) {
if (snapshot.data != null) {
return ListView.builder(
itemCount: snapshot.data?.length,
shrinkWrap: true,
controller: controller,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(snapshot.data![index]['description']),
),
),
);
},
);
} else {
return const CircularProgressIndicator();
}
},
),
);
}
void _scrollListener() {
if (controller.offset >= controller.position.maxScrollExtent &&
!controller.position.outOfRange) {
print("Cavc$controller");
print("at the end of list");
postListBloc!.fetchNextPosts(widget.fromgst, widget.postType);
}
}
}
I found it not open source github repo