StreamBuilder Firestore Pagination - firebase

I m new to flutter, and I'm trying to paginate a chat when scroll reach top with streambuilder. The problem is: when i make the query in scrollListener streambuilder priorize his query above the scrollListener and returns de old response. Is there any way to do this? What are my options here? Thanks!
Class ChatScreenState
In initState I create the scroll listener.
#override
void initState() {
listScrollController = ScrollController();
listScrollController.addListener(_scrollListener);
super.initState();
}
Here i create the StreamBuilder with the query limited to 20 last messages. Using the _messagesSnapshots as global List.
#override
Widget build(BuildContext context) {
return Scaffold(
key: key,
appBar: AppBar(title: Text("Chat")),
body: Container(
child: Column(
children: <Widget>[
Flexible(
child: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('messages')
.where('room_id', isEqualTo: _roomID)
.orderBy('timestamp', descending: true)
.limit(20)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
_messagesSnapshots = snapshot.data.documents;
return _buildList(context, _messagesSnapshots);
},
)),
Divider(height: 1.0),
Container(
decoration: BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
),
],
),
));
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
_messagesSnapshots = snapshot;
return ListView.builder(
controller: listScrollController,
itemCount: _messagesSnapshots.length,
reverse: true,
itemBuilder: (context, index) {
return _buildListItem(context, _messagesSnapshots[index]);
},
);
}
And in the _scollListener method i query the next 20 messages and add the result to the Global list.
_scrollListener() {
// If reach top
if (listScrollController.offset >=
listScrollController.position.maxScrollExtent &&
!listScrollController.position.outOfRange) {
// Then search last message
final message = Message.fromSnapshot(
_messagesSnapshots[_messagesSnapshots.length - 1]);
// And get the next 20 messages from database
Firestore.instance
.collection('messages')
.where('room_id', isEqualTo: _roomID)
.where('timestamp', isLessThan: message.timestamp)
.orderBy('timestamp', descending: true)
.limit(20)
.getDocuments()
.then((snapshot) {
// To save in the global list
setState(() {
_messagesSnapshots.addAll(snapshot.documents);
});
});
// debug snackbar
key.currentState.showSnackBar(new SnackBar(
content: new Text("Top Reached"),
));
}
}

I'm gonna post my code i hope someone post a better solution, probably is not the best but it works.
In my app the actual solution is change the state of the list when reach the top, stop stream and show old messages.
All code (State)
class _MessageListState extends State<MessageList> {
List<DocumentSnapshot> _messagesSnapshots;
bool _isLoading = false;
final TextEditingController _textController = TextEditingController();
ScrollController listScrollController;
Message lastMessage;
Room room;
#override
void initState() {
listScrollController = ScrollController();
listScrollController.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
room = widget.room;
return Flexible(
child: StreamBuilder<QuerySnapshot>(
stream: _isLoading
? null
: Firestore.instance
.collection('rooms')
.document(room.id)
.collection('messages')
.orderBy('timestamp', descending: true)
.limit(20)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
_messagesSnapshots = snapshot.data.documents;
return _buildList(context, _messagesSnapshots);
},
),
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
_messagesSnapshots = snapshot;
if (snapshot.isNotEmpty) lastMessage = Message.fromSnapshot(snapshot[0]);
return ListView.builder(
padding: EdgeInsets.all(10),
controller: listScrollController,
itemCount: _messagesSnapshots.length,
reverse: true,
itemBuilder: (context, index) {
return _buildListItem(context, _messagesSnapshots[index]);
},
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final message = Message.fromSnapshot(data);
Widget chatMessage = message.sender != widget.me.id
? Bubble(
message: message,
isMe: false,
)
: Bubble(
message: message,
isMe: true,
);
return Column(
children: <Widget>[chatMessage],
);
}
loadToTrue() {
_isLoading = true;
Firestore.instance
.collection('messages')
.reference()
.where('room_id', isEqualTo: widget.room.id)
.orderBy('timestamp', descending: true)
.limit(1)
.snapshots()
.listen((onData) {
print("Something change");
if (onData.documents[0] != null) {
Message result = Message.fromSnapshot(onData.documents[0]);
// Here i check if last array message is the last of the FireStore DB
int equal = lastMessage?.compareTo(result) ?? 1;
if (equal != 0) {
setState(() {
_isLoading = false;
});
}
}
});
}
_scrollListener() {
// if _scroll reach top
if (listScrollController.offset >=
listScrollController.position.maxScrollExtent &&
!listScrollController.position.outOfRange) {
final message = Message.fromSnapshot(
_messagesSnapshots[_messagesSnapshots.length - 1]);
// Query old messages
Firestore.instance
.collection('rooms')
.document(widget.room.id)
.collection('messages')
.where('timestamp', isLessThan: message.timestamp)
.orderBy('timestamp', descending: true)
.limit(20)
.getDocuments()
.then((snapshot) {
setState(() {
loadToTrue();
// And add to the list
_messagesSnapshots.addAll(snapshot.documents);
});
});
// For debug purposes
// key.currentState.showSnackBar(new SnackBar(
// content: new Text("Top reached"),
// ));
}
}
}
The most important methods are:
_scrollListener
When reach the top i query old messages and in setState i set isLoading var to true and set with the old messages the array i m gonna show.
_scrollListener() {
// if _scroll reach top
if (listScrollController.offset >=
listScrollController.position.maxScrollExtent &&
!listScrollController.position.outOfRange) {
final message = Message.fromSnapshot(
_messagesSnapshots[_messagesSnapshots.length - 1]);
// Query old messages
Firestore.instance
.collection('rooms')
.document(widget.room.id)
.collection('messages')
.where('timestamp', isLessThan: message.timestamp)
.orderBy('timestamp', descending: true)
.limit(20)
.getDocuments()
.then((snapshot) {
setState(() {
loadToTrue();
// And add to the list
_messagesSnapshots.addAll(snapshot.documents);
});
});
// For debug purposes
// key.currentState.showSnackBar(new SnackBar(
// content: new Text("Top reached"),
// ));
}
}
And loadToTrue that listen while we are looking for old messages. If there is a new message we re activate the stream.
loadToTrue
loadToTrue() {
_isLoading = true;
Firestore.instance
.collection('rooms')
.document(widget.room.id)
.collection('messages')
.orderBy('timestamp', descending: true)
.limit(1)
.snapshots()
.listen((onData) {
print("Something change");
if (onData.documents[0] != null) {
Message result = Message.fromSnapshot(onData.documents[0]);
// Here i check if last array message is the last of the FireStore DB
int equal = lastMessage?.compareTo(result) ?? 1;
if (equal != 0) {
setState(() {
_isLoading = false;
});
}
}
});
}
I hope this helps anyone who have the same problem (#Purus) and wait until someone give us a better solution!

First of all, I doubt such an API is the right backend for a chat app with live data - paginated APIs are better suited for static content.
For example, what exactly does "page 2" refer to if 30 messages were added after "page 1" loaded?
Also, note that Firebase charges for Firestore requests on a per-document basis, so every message which is requested twice hurts your quota and your wallet.
As you see, a paginated API with a fixed page length is probably not the right fit. That why I strongly advise you to rather request messages that were sent in a certain time interval.
The Firestore request could contain some code like this:
.where("time", ">", lastCheck).where("time", "<=", DateTime.now())
Either way, here's my answer to a similar question about paginated APIs in Flutter, which contains code for an actual implementation that loads new content as a ListView scrolls.

I have a way to archive it. Sorry for my bad english
bool loadMoreMessage = false;
int lastMessageIndex = 25 /// assumed each time scroll to the top of ListView load more 25 documents When I scroll to the top of the ListView =>setState loadMoreMessage = true;
This is my code:
StreamBuilder<List<Message>>(
stream:
_loadMoreMessage ? _streamMessage(lastMessageIndex): _streamMessage(25),
builder: (context, AsyncSnapshot<List<Message>> snapshot) {
if (!snapshot.hasData) {
return Container();
} else {
listMessage = snapshot.data;
return NotificationListener(
onNotification: (notification) {
if (notification is ScrollEndNotification) {
if (notification.metrics.pixels > 0) {
setState(() {
/// Logic here!
lastMessageIndex = lastMessageIndex + 25;
_loadMoreMessage = true;
});
}
}
},
child: ListView.builder(
controller: _scrollController,
reverse: true,
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ChatContent(listMessage[index]);
},
),
);
}
},
),

Related

Getting an error when trying to retrieve data from Firebase FireStore document

I'm trying to retrieve all the courses that the user has enrolled in, these courses are present in an array within the document.
After retrieving the course ID from the users collection, I'm trying to retrieve the course details from the courses collection.
But before the courses variable is populated, the coursesCollection statement is executed and throwing the below error.
======== Exception caught by widgets library =======================================================
The following assertion was thrown building _BodyBuilder:
'in' filters require a non-empty [List].
'package:cloud_firestore/src/query.dart':
Failed assertion: line 706 pos 11: '(value as List).isNotEmpty'
Here is the error causing code:
List courses = [];
var coursesCollection;
void fetchCourses() async {
final loggedInUser = FirebaseAuth.instance.currentUser;
if (loggedInUser != null) {
final userCollection = await FirebaseFirestore.instance.collection('users').doc(loggedInUser.uid).get();
courses = userCollection.get('coursesEnrolled');
}
}
#override
void initState() {
fetchCourses();
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
super.initState();
}
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: coursesCollection.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(backgroundColor: kBrandColor),
);
}
}
final courseListStream = snapshot.data!.docs.map((course) {
return CourseData.fromDocument(course);
}).toList();
List<BadgedCourseCard> courseCards = [];
for (var course in courseListStream) {
final courseDocID = course.courseDocID;
final courseID = course.courseID;
final courseTitle = course.courseTitle;
final courseImage = course.courseImage;
final courseBgColor = hexToColor(course.courseBackgroundColor.toString());
hexToColor(course.courseFgColor.toString());
final badgedCourseCard = BadgedCourseCard(
courseTitle: courseTitle.toString(),
courseTitleTextColor: courseFgColor,
cardBackgroundColor: courseBgColor,
courseImage: courseImage.toString(),
courseCardTapped: () {
Provider.of<CourseProvider>(context, listen: false).currentCourseDetails(
currentCourseDocID: courseDocID,
currentCourseID: courseID,
);
Navigator.of(context).push(ScaledAnimationPageRoute(CourseLandingPage(courseID: courseID.toString())));
},
courseBookmarkTapped: () => print("Course Bookmark Tapped"),
rightPadding: 3,
bottomPadding: 0.5,
cardWidth: 80,
);
courseCards.add(badgedCourseCard);
}
return SizedBox(
height: 20.5.h,
child: ListView(
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
children: courseCards,
),
);
},
);
}
How can I fix this issue?
Here,
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
courses would be [] because fetchCourses is an async call.
Change the return type of fetchCourses from void to Future<void> & try using a then callback:
#override
void initState() {
super.initState();
fetchCourses().then((val) {
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
setState(() {});
});
}
I would also recommend to use FutureBuilder as a better alternative.
coursesCollection is null that's why you're getting another error. Render StreamBuilder only when coursesCollection is not null.
coursesCollection != null ? StreamBuilder(...) : SizedBox(),
For listening to the user's enrolled courses, another StreamBuilder can be used. It would be a nested StreamBuilder setup.
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser.uid).snapshots(),
builder: (context, snapshot) => snapshot.hasData ? StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: snapshot.data!.data()!['coursesEnrolled']).snapshots(),
builder: (context, snapshotTwo) {},
) : Text('Loading...'),
),

Flutter - Class 'DocumentSnapshot' has no instance getter 'docs'

I have 2 streams in my codes the first one is to get the userid from friend list and the second stream is to use the list of ids to search for the userid's document in firebase.
Stream friendIDStream;
Stream friendNameStream;
Widget friendList() {
return StreamBuilder(
stream: friendNameStream,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, index) {
return FriendTile(
snapshot.data.docs[index].data()["username"]);
},
)
: Container();
},
);
}
#override
void initState() {
getUserFriend();
getNameByID();
super.initState();
}
getUserFriend() async {
Constant.currentId =
await HelperFunctions.getUserIdSharedPreference(Constant.currentId);
setState(() {
firebaseMethods.getFriend(Constant.currentId).then((value) {
setState(() {
friendIDStream = value;
});
});
});
}
getNameByID() {
setState(() {
firebaseMethods.getFriendName(friendIDStream).then((value) {
setState(() {
friendNameStream = value;
});
});
});
}
This is the firestore code.
Future getFriend(String ownerid) async {
return await FirebaseFirestore.instance
.collection("users")
.doc(ownerid)
.collection("friends")
.snapshots();
}
Future getFriendName(friendid) async {
return await FirebaseFirestore.instance
.collection("users")
.doc(friendid)
.snapshots();
}
I doesn't know why is this happening since I can display the list of ids. I had tried changing docs to doc but is also produce the same error.
Edit:
Added photos of my database structure.
the reason is your function getFriendName is returning a documentsnapshot not a querysnapshot. SO replace your old code with this:-
Widget friendList() {
return StreamBuilder(
stream: friendNameStream,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: 1,
itemBuilder: (context, index) {
return FriendTile(
//snapshot.data.docs[index].data()["username"] old one
snapshot.data["username"]); //new one
},
)
: Container();
},
);
}

'Future<QuerySnapshot>' is not a subtype of type 'Stream<dynamic>'?

Im struggling trying to use a streambuider. Im getting an error that says :
════════ Exception caught by widgets library ═══════════════════════════════════
The following _TypeError was thrown building StreamBuilder<UserData>(dirty, state: _StreamBuilderBaseState<UserData, AsyncSnapshot<UserData>>#e2c02):
type 'Future<QuerySnapshot>' is not a subtype of type 'Stream<dynamic>' of 'function result'
heres my code
Stream myVideos;
getalldata() async {
//get videos as future
myVideos = FirebaseFirestore.instance
.collection('videos')
.where('uid', isEqualTo: widget.userid)
.snapshots();
var documents = await FirebaseFirestore.instance
.collection('videos')
.where('uid', isEqualTo: widget.userid)
.get();
if (documents.docs.isNotEmpty) {
for (var item in documents.docs) {
likes = item.data()['likes'].length + likes;
}
} else {
setState(() {
picturesdontexists = true;
});
}
setState(() {
dataisthere = true;
});
}
#override
Widget build(BuildContext context) {
final user = Provider.of<Userforid>(context);
return dataisthere == false
? Scaffold(body: Center(child: CircularProgressIndicator()))
: StreamBuilder<UserData>(
stream: DatbaseService(uid: user?.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0.0,
- - - - - - - - - -
),
),
StreamBuilder(
stream: myVideos,
builder: ( context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if(videos>0){
print(snapshot.data);
return StaggeredGridView.countBuilder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
crossAxisCount: 3,
itemCount: snapshot.data.docs.length,
itemBuilder: (context, index) {
DocumentSnapshot video =
snapshot.data.docs[index];
return InkWell(
onTap: () {
Navigator.of(context)
.pushNamed(ChatFirstpage.route);
},
child: Card(
elevation: 0.0,
What I think is that the error is thrown in the stream because before im using the stream myVideos im getting no error and all works fine .
Maye anyone can help Thank!. if you need more Information leave a comment .
Heres mine DatbaseService
class DatbaseService {
static DatbaseService instance = DatbaseService();
final String uid;
String _messages = "messages";
String _images = "images";
DatbaseService({this.uid});
//userData from snapshot
UserData userDataFromSnapshot(DocumentSnapshot snapshot) {
return UserData(
uid: uid,
email: snapshot.data()['email'],
fullname: snapshot.data()['fullname'],
password: snapshot.data()['password'],
url: snapshot.data()['url'],
username: snapshot.data()['username'],
);
}
//get user doc stream
Stream<UserData> get userData {
return myprofilsettings.doc(uid).snapshots().map(userDataFromSnapshot);
}
``
This usually arises when you are passing a Future where Stream should have gone or where you have defined the type of the variable as Stream but you are putting that variable equal to a future value.
I think instead of creating a variable like Stream myVideos you can directly put
FirebaseFirestore.instance
.collection('videos')
.where('uid', isEqualTo: widget.userid)
.snapshots()
inside the stream builder.
Also please provide the whole code (DatbaseService).

FutureBuilder's snapshot is always either null or has no data

I am fairly new to flutter and I hope this problem can be easily solved, but up until now I have no idea what's going wrong.
I have an app where I would like to return a list of users after the search button is pressed. I think that a FutureBuilder with a ListView.builder is the correct choice for this, but somehow I can't get my data to appear in the FutureBuilder even though my async function is definitely finding results.
My future:
Future<List<Map<dynamic, dynamic>>> results;
the button:
onPressed: () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
setState(() {
results = null;
searched = true;
});
results = getGuestData(widget.place['placeName'], query);
}
},
Async function:
Future<List<Map>> getGuestData(String placeName, Map query) async {
List<Map> result = [];
CollectionReference guestsRef = Firestore.instance
.collection(XXX)
.document(XXX)
.collection(XXX);
if (query['date'] == null) {
guestsRef
.where('lastName', isEqualTo: query['lastName'])
.where('firstName', isEqualTo: query['firstName'])
.getDocuments()
.then((doc) {
if (doc != null) {
result = doc.documents.map((x) => x.data).toList(); \\debugger shows this is working
}
}).catchError((e) => print("error fetching data: $e"));
}
return result;
and finally the problem:
FutureBuilder<List<Map>>(
future: results,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
List guests = snapshot.data;
return ListView.builder(
shrinkWrap: true,
itemCount: guests.length,
itemBuilder: (context, index) {
return ListTile(
... //do stuff here
);
},
);
} else {
return CircularProgressIndicator();
}
},
)
snapshot.data will indeed turn out to be a List, but the size is always 0 despite the getGuestData query finding users.

type 'DateTime' is not a subtype of type 'List<dynamic>' Flutter and firebase

so i have a function that get data from firebase firestore for my chat app i wanted to add pagination to it.
i created this function :
getMessage() {
if (!hasMore) {
return;
}
if (isLoading) {
return;
}
if (lastDoc == null) {
_querySnapshot = widget.messageDocRef
.collection('Chat')
.orderBy('timestamp', descending: true)
.limit(15)
.getDocuments();
} else if (lastDoc != null) {
_querySnapshot = widget.messageDocRef
.collection('Chat')
.orderBy('timestamp', descending: true)
.startAfter(
lastDoc.data['timestamp'].toDate().add(Duration(hours: 1)))
.limit(15)
.getDocuments();
}
_querySnapshot.then((docSnap) {
lastDoc = docSnap.documents[docSnap.documents.length - 1];
print(lastDoc);
print(lastDoc.data['content']);
print(lastDoc.data['timestamp'].toDate().add(Duration(hours: 1)));
});
return _querySnapshot.asStream();
}
this line print the correct time that is stored in firestore.
print(lastDoc.data['timestamp'].toDate().add(Duration(hours: 1)));
but for some reason when i try to load more messages with
startAfter(
lastDoc.data['timestamp'].toDate().add(Duration(hours: 1)))
the screens becomes Red with this error message : type 'DateTime' is not a subtype of type 'List'
i've tried with lastDoc.data['timestamp'] & with lastDoc.data['timestamp'].toDate() and with lastDoc.data['timestamp'].toDate().add(Duration(hours: 1)) but nothing happened and the same error occurs.
NOTE that i am assigning getMessage() to a streamBuilder.
this is my StreamBuilder
StreamBuilder(
//TODO add pagination later...
stream: getMessages(),
/*stream: widget.messageDocRef
.collection('Chat')
.orderBy('timestamp', descending: true)
.limit(80)
.getDocuments()
.asStream(),*/
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return Center(
child: circularProgress(),
);
}
final messages = snapshot.data.documents;
List<MessageBubble> messagesBubble = [];
for (var message in messages) {
final messageText = message.data['content'];
final messageSender = message.data['from'];
final messageType = message.data['type'];
final messageBubble = MessageBubble(
type: messageType,
text: messageText,
isMe: widget.currentUser.id == messageSender,
);
messagesBubble.add(messageBubble);
}
return Expanded(
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
reverse: true,
controller: _scrollController,
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 10.0),
children: messagesBubble,
),
);
},
);
thanks!
startAfter takes List of type dynamic as an input so wrap lastDoc.data['timestamp'].toDate().add(Duration(hours: 1)) inside a list [].
Also since you've the documents list handy in that function, you could you use: startAfterDocument such in:
_querySnapshot = widget.messageDocRef
.collection('Chat')
.orderBy('timestamp', descending: true)
.startAfterDocument(lastDoc)
I hope that helps.
Here's a good video from Firebase team on YouTube explains pagination:
How Do I Paginate My Data?
And this page is a good resource from Firestore docs:
Paginating data with query cursors

Resources