I'm trying to create chat demo app with firebase in flutter but when i send message, then those message documents are being created at randomly any places in firestore database. thats why my chat screen messages are in wrong manner, means not arranged according to time.
Some piece of code:
method for saving message details to firestore:
Future<void> sendMesssage() async{
if(messagesController.text.length>0){
String msgId = firestore.collection("messages").document().documentID.toString();
await firestore.collection("messages").document(msgId).setData({
'text': messagesController.text,
"from": widget.user.user.email, //sender email Id
"to":widget.chatMateEmail, // receiver email id
"msgId":msgId,
"senderUid": widget.user.user.uid, //sender uid
"receiverUid":widget.receiverUid //receiver uid
});
messagesController.clear();
}
}
UI For chat screen:
method for fetching messages from firestore:
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: firestore.collection("messages").snapshots(),
builder: (context, snapshot){
if(snapshot.hasError){
return Center(child: Text("${snapshot.error}"),);
}
if(!snapshot.hasData){
return Center(child: CircularProgressIndicator(),);
}else{
List<DocumentSnapshot> docs = snapshot.data.documents;
return
Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index){
print("Messagessssssss:${docs[index]['text']}");
return Message( // custom class
from: docs[index]['from'],
text: docs[index]['text'],
me: widget.user.user.email == docs[index]['from'],
);
},
),
);
}
},
),
),
chat Screen:
My Cloud Firestore Screenshot:
It got solved according to #DougStevenson sir's answer, I added new field with name "messageTime" and add DateTime.now(), And fetched messages according to messageTime (sort by ascending order ).
I modified a little bit my code & working perfectly:
Future<void> sendMesssage() async{
if(messagesController.text.length>0){
String msgId = firestore.collection("messages").document().documentID.toString();
await firestore.collection("messages").document(msgId).setData({
..........
"messageTime": DateTime.now() // message DateTime
});
messagesController.clear();
}
}
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: firestore.collection("messages").orderBy('messageTime', descending: false).snapshots(), //and sort by ascending order according to datetime.
builder: (context, snapshot){
......
},
),
),
Related
I am having a trouble reading collection from firebase and saving values in a list.
I basically have a collection called 'brands' where I have car brands like this:
Firebase 'brands' collection screenshot
I need these car brands to be saved as a list like this, to be able to use it in a dropdown menu as items:
<String>[
'ferrari',
'mercedes',
'porsche',
]
I have tried using StreamBuilder (below) but it requires to return a widget and I do not actually need a widget to be returned, so below StreamBuilder is just an experiment "in progress".
Do you have any ideas?
final stream = FirebaseFirestore.instance
.collection('accounts')
.doc('dealers')
.collection(user!.uid)
.doc(dealerName)
.collection('brands')
.snapshots();
StreamBuilder(
stream: stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text('Error in receiving snapshot: ${snapshot.error}');
}
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor,
),
);
}
return ListView.builder(
padding: EdgeInsets.all(8),
reverse: true,
itemCount: snapshot.data.docs!.length,
itemBuilder: (BuildContext context, int index) {
return Text(
snapshot.data.docs[index]['brandName'],
);
},
);
},
);
Once you get the data from firebase, loop through it and add the car brands to your list. Try this:
List<String> myBrands = [];
final dataRef = await FirebaseFirestore.instance
.collection('accounts')
.doc('dealers')
.collection(user!.uid)
.doc(dealerName)
.collection('brands')
.get();
dataRef.docs.forEach((doc) {
myBrands.add(doc.data()['brandName']);
});
You should then be able to use the myBrands list for your dropdown menu.
I am using two quires to fetch data from the firestore.
Query 1.
_firestore
.collection('chats')
.doc(getCurrentUser().uid)
.collection('chatUsers')
.orderBy('timestamp');
with all the querysnapshot document from query 1. I am fetching last message and document id, and displaying the last message in listTile. With document id i am passing the id to fetch other data from other collection like name photo etc.
Query 2.
Future<DocumentSnapshot> fetchUserData(String uid) async => await _firestore
.collection('users')
.doc(uid).get();
So for this I need to use nested stream builder. First stream builder to fetch all data. Second stream builder to fetch user requested data from all data. what will be the best approach?
This is how i am using query 1 in my widgets for the query 2 I have to implement it inside the ListView.builder which will be the nested stream. please guide me with the best approach to this.
SafeArea(
child: Scaffold(
body: StreamBuilder<QuerySnapshot>(
stream: _fetchUserChatRoom.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
return _tiles(snapshot.data.docs);
} else if (snapshot.hasError) {
return Icon(Icons.error_outline);
} else {
return CircularProgressIndicator();
}
}),
),
);
}
Widget _tiles(List<QueryDocumentSnapshot> docs) {
return ListView.builder(
itemCount: docs.length,
itemBuilder: (BuildContext context, int index) {
var data = ChatModel.fromMap(docs[index].data());
return GestureDetector(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ChatScreen(uid: docs[index].id))),
child: ListTile(
leading: CircleAvatar(),
title: Text(data.message),
subtitle: Text(data.timestamp.toString()),
trailing: Text('time'),
),
);
});
You can either use async and await in your ListView.builder, however, I imaging this could slowdown you app and cause a lot of firestore calls.
Widget _tiles(List<QueryDocumentSnapshot> docs) {
return ListView.builder(
itemCount: docs.length,
itemBuilder: (BuildContext context, int index) async {
var data = ChatModel.fromMap(docs[index].data());
var userData = await fetchUserData(data[index].uid);
return GestureDetector(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ChatScreen(uid: docs[index].id))),
child: ListTile(
leading: CircleAvatar(),
title: Text(data.message),
subtitle: Text(data.timestamp.toString()),
trailing: Text('time'),
),
);
});
Other options (which I use) is to use a Provider class with all the contacts. You can fill the Provider when the app initializes with all the users in your firestore. After that you can use each user data anywhere in your app.
I found following issue. Then I understand it.
Flutter / FireStore: how to display an image from Firestore in Flutter?
File uploading is succeeding.
var imgUrl = await ref.getDownloadURL();
print(imgUrl.toString());
However I have following error.
It seems I'm doing same.
Unhandled Exception: PlatformException(Error -13010, FIRStorageErrorDomain, Object images/cars/40711b90-9db4-11ea-c602-a557c9b7697a.jpeg does not exist.)
However I have no idea how to display and handle it.
Please give me advice. Thanks.
You need to add the url to firestore first:
StorageTaskSnapshot snapshot = await storage
.ref()
.child("images/$imageName")
.putFile(file)
.onComplete;
if (snapshot.error == null) {
final String downloadUrl =
await snapshot.ref.getDownloadURL();
await Firestore.instance
.collection("images")
.add({"url": downloadUrl, "name": imageName});
}
Now in Firestore you will have collection called images and document with the image url and image name. The method getDownloadUrl() returns the url of the image so you can store it in Firestore. Then to display it you can do the following:
body: Container(
padding: EdgeInsets.all(10.0),
child: FutureBuilder(
future: getImages(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
contentPadding: EdgeInsets.all(8.0),
title:
Text(snapshot.data.documents[index].data["name"]),
leading: Image.network(
snapshot.data.documents[index].data["url"],
fit: BoxFit.fill),
);
});
} else if (snapshot.connectionState == ConnectionState.none) {
return Text("No data");
}
return CircularProgressIndicator();
},
),
),
/// code here
Future<QuerySnapshot> getImages() {
return fb.collection("images").getDocuments();
}
Here you use the method getImages() which retrieves all the images from the collection images. To display the image you can use Image.network widget.
child: StreamBuilder(
stream: databaseReference
.collection(collectionName)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return ListView(
shrinkWrap: true,
children: elementList(snapshot),
);
}
}
),
this is the my StreamBuilder code,
elementList(AsyncSnapshot<QuerySnapshot> snapshot) {
return snapshot.data.documents.map((document).mydata {
return ListTile()
}).toList();
}
mydata is the document name like Country, and name of array List,
this is the ListTile building code, and I want get the Country list in this StreamBuilder, and elements to ListTile.
the database looks like,
FireStore database Country List in the document in the collection
I simply want to perform an 'OR' operation and get the both results of two queries into one stream.
Here's my code with a single stream
StreamBuilder(
stream: Firestore.instance
.collection('list')
.where('id', isEqualTo: 'false')
.orderBy('timestamp')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: CircularProgressIndicator(),
)
],
);
if (snapshot.data.documents.length == 0)
return const Center(
child: Text(
"Not Available",
style: TextStyle(fontSize: 30.0, color: Colors.grey),
),
);
return ListView.builder(
padding: EdgeInsets.all(5.0),
key: Key(randomString(20)),
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
return ListCard(snapshot.data.documents[index]);
},
);
}),
Instead of a single stream now I want to feed two stream to the same stream builder.
I tried StreamGroup but it's not working since Widgets rebuild
StreamGroup.merge([streamOne, streamTwo]).asBroadcastStream();
I tried followed method also
Stream<List<DocumentSnapshot>> searchResult() {
List<Stream<List<DocumentSnapshot>>> streamList = [];
Firestore.instance
.collection('room-list')
.where('id', isEqualTo: 'false')
.snapshots()
.forEach((snap) {
streamList.add(Observable.just(snap.documents));
});
Firestore.instance
.collection('room-list')
.where('id', isEqualTo: 'pending')
.snapshots()
.forEach((snap) {
streamList.add(Observable.just(snap.documents));
});
var x = Observable.merge(streamList)
.scan<List<DocumentSnapshot>>((acc, curr, i) {
return acc ?? <DocumentSnapshot>[]
..addAll(curr);
});
return x;
}
Here I get the error there should be at least a single stream to merge. Its because Observable.merge(streamList) is called before items are added to streamList.
I simply want to get the both results of two queries into one stream.
This should work.
//Change your streams here
Stream<List<QuerySnapshot>> getData() {
Stream stream1 = Firestore.instance.collection('list').where('id', isEqualTo: 'false').orderBy('timestamp').snapshots();
Stream stream2 = Firestore.instance.collection('list').where('id', isEqualTo: 'true').orderBy('timestamp').snapshots();
return StreamZip([stream1, stream2]);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: StreamBuilder(
stream: getData(),
builder: (BuildContext context, AsyncSnapshot<List<QuerySnapshot>> snapshot1) {
List<QuerySnapshot> querySnapshotData = snapshot1.data.toList();
//copy document snapshots from second stream to first so querySnapshotData[0].documents will have all documents from both query snapshots
querySnapshotData[0].documents.addAll(querySnapshotData[1].documents);
if (querySnapshotData[0].documents.isEmpty)
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: CircularProgressIndicator(),
)
],
);
if (querySnapshotData[0].documents.length == 0)
return const Center(
child: Text(
"Not Available",
style: TextStyle(fontSize: 30.0, color: Colors.grey),
),
);
return new ListView(
children: querySnapshotData[0].documents.map((DocumentSnapshot document){
// put your logic here. You will have access to document from both streams as "document" here
return new ListCard(document);
}).toList()
);
}
),
);
}
Hope this helps!!!
I’m not sure why you’re using forEach and Observable.just().
You can just merge two firestore streams directly like:
Observable.merge([stream1, stream2]).pipe(combineStream);
Wherre stream1/2 is just your firestore snapshot.
I used RxDart package to combine two streams as shown below
RxDart - CombineLatestStream
final Stream<DocumentSnapshot> user = Firestore.instance
.collection("users")
.document(firebaseUser.uid)
.snapshots();
final Stream<QuerySnapshot> cards =
Firestore.instance.collection("cards").snapshots();
CombineLatestStream.list([user, cards]).listen((data) {
add(LoadedHomeEvent(
data.elementAt(0),
data.elementAt(1),
));
});
Well I am late, but just gonna put it out there.
You can add whereIn clause in your query like this:
Firestore.instance.collection("collection_name").where("field",whereIn:["false","true"]).snapshots();
I was also trying to combine two streams from firestore (as querying does not support OR) and went about it like this:
import 'package:rxdart/rxdart.dart';
Rx.combineLatest2(
StreamQuerySnapshot1, //a snapshot from firestore
StreamQuerySnapshot2, //another snapshot from firestore
(var stream1, var stream2) {
return [...stream1.docs, ...stream2.docs]; //Concatenated list
}
)
This will emit changes no matter which streams is changing in contrast to other solutions I found which support emits only if both streams have changes.
The best way I found is to use MergeStream from RxDart
Stream<QuerySnapshot> searchResult() {
final falseRoomStream = FirebaseFirestore.instance
.collection('room-list')
.where('id', isEqualTo: 'false')
.snapshots();
final pendingRoomStream = FirebaseFirestore.instance
.collection('room-list')
.where('id', isEqualTo: 'pending')
.snapshots();
return MergeStream([falseRoomStream, pendingRoomStream]);
}