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

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();
},
);
}

Related

FutureBuilder can't "see" data / snapshot.hasData

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 {

Create a dynamic list that is updates correctly with the streambuilder in my widget

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(
);
},
);
}

Building future from firebase result

I'm using a future to search firebase for users based on user input. The future then returns the results as a list but the future builder is not working to show the data in the UI. How can I build this data in the UI? Also is using future builder the correct way of doing this?
FutureBuilder(
future: userSearch(_userSearchController.text),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else {
return new CircularProgressIndicator();
}
},
)
Future userSearch(String userSearch) async {
QuerySnapshot result = await Firestore.instance
.collection("users")
.where("name", isEqualTo: userSearch)
.getDocuments();
final List<DocumentSnapshot> docs = result.documents;
return docs;
}
Edit using Stream
StreamBuilder(
stream: userSearch(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
print(snapshot.data.length);
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return userWidget(snapshot.data[index].data);
});
} else {
return new CircularProgressIndicator();
}
},
)
Stream<dynamic> userSearch() async* {
print("User Search");
QuerySnapshot result = await Firestore.instance
.collection("users")
.where("name", isEqualTo: _userSearchController.text.toLowerCase())
.getDocuments();
yield result.documents;
}
Due to you have catered for search based on userInput, you should be using Streambuilder. Future only return once, but streambuilder will always rebuild your widget whenever the return value is changed.
To build your UI, you can use ListView.builder:
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return BuildYourWidget(snapshot.data[index]);
});
} else {
return CircularProgressionIndicator();
}
BuilYourWidget(dynamic yoursnapshotdata) {
return ListTile(trailing: Text('hello'), title: Text(yoursnapshotdata));
}

Flutter _scrollController.addListener never executes

I followed so many solutions on this, and nothing ever seems to work for me!
I created the listener variable like so:
ScrollController _scrollController = ScrollController();
And then attached it to the ListView.builder controller like so:
ListView.builder(
controller: _scrollController,
itemCount: _posts.length,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
Post post = _posts[index];
return FutureBuilder(
future: DatabaseService.getUserWithId(post.authorId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return SizedBox.shrink();
}
User author = snapshot.data;
return PostItem(post: post, author: author);
});
},
),
and then called the addListener function in the initState like so:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// set up listener here
_scrollController.addListener(() {
print('are we even here?!');
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print('reached the bottom');
});
} else if (_scrollController.offset <=
_scrollController.position.minScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print("reached the top");
});
} else {
setState(() {
print('were here');
});
}
});
loadUserData();
_setupFeed();
}
the print('are we even here?!'); never shows in the logcat.
What am I doing wrong here?

StreamBuilder Firestore Pagination

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]);
},
),
);
}
},
),

Resources