I have a Future function to get data from Firebase, that is not empty queries correctly:
Future getProducts(vendorId) async {
await vendorsInfoColl.doc(vendorId)
.collection("products")
.get()
.then((QuerySnapshot querySnapShot) async {
if (querySnapShot.docs.isNotEmpty){
print('not empty');
print(querySnapShot.docs);
return querySnapShot.docs;
} else {
print('snapshot empty');
return null;
}
});
}
I just have trouble getting a FutureBuilder see it. Keeps saying there is empty data.
Widget build(BuildContext context) {
return AlertDialog(
// pull title from db
// title: Future Text(vendorTitle.toString()),
title: Text(vendorTitle.toString()),
content: FutureBuilder(
future: VendorDatabase().getProducts(widget.vendorId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
print(snapshot.connectionState);
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
var blabla = snapshot.data;
print('there is snapshot data: $blabla');
return ListView.builder(
itemCount: 3,
itemBuilder: (context, index) {
return ListTile(
title: Text(blabla['products']) //<-- i know this is wrong but can't fixt it till future builder actually sees some data.
);
}
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
}
)
);
}
It would be great if can also get some tips on how to put it in a list :)
You're not returning anything inside getProducts:
Future getProducts(vendorId) async {
return await vendorsInfoColl.doc(vendorId)
.collection("products")
.get()
.then((QuerySnapshot querySnapShot) async {
if (querySnapShot.docs.isNotEmpty){
return querySnapShot.docs;
} else {
return null;
}
});
}
To avoid this in the future, declare a type in your Future:
// vvvvvvvvvvvvvvvv
Future<List<Document>> getProducts(vendorId) async {
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();
},
);
}
I am getting information list from another datasnapshot and after that I assigned it to my itemtile.
The problem is that its only updating once.
When I open it again, it contains the old data and not the new data.
List<String> information5 = [];
void updateInformation5(List information5) {
// updated by another snapshot data
setState(() => information5 = information5);
}
Widget itemList() {
return StreamBuilder(
stream: sample,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.documents.length,
shrinkWrap: true,
itemBuilder: (context, index) {
try {
return itemTile (
itemName: information5[index],
itemId: snapshot.data.documents[index].data()["itemId"],
imageUrl:information4[index],
);
} on Exception catch (exception) {
} catch (error) {
}
}
) : Container(
);
},
);
}
How would you implement a pull-down to refresh in Flutter app that gets data from a collection in Firestore preferable using a StreamBuilder or a FutureBuilder and displays it in a ListView ?
I ended up using information from here. How to refresh or reload a flutter firestore streambuilder manually?
I added the Refresh indicator and made the my stream get its data from a function see code below.
var stream;
#override
void initState() {
setState(() {
stream = mouvesStream();
});
super.initState();
}
Stream<QuerySnapshot> stream() {
return Firestore.instance.collection(_locationState).snapshots();
}
#override
Widget build(BuildContext context) {
body: StreamBuilder(
stream: stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.connectionState == ConnectionState.active) {
List aList = new List();
aList.clear();
for (DocumentSnapshot _doc in snapshot.data.documents) {
Model _add = new Model.from(_doc);
aList.add(_add);
}
return TabBarView(
children: <Widget>[
RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
itemCount: aList.length,
itemBuilder: (context, index) {
return Card(aList[index]);
},
),
),
Icon(Icons.directions_transit),
],
);
} else {
return Container(
child: Center(child: CircularProgressIndicator()));
}
})));
}
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]);
},
),
);
}
},
),