How to build a Stream based on a Future result in Flutter? - firebase

I have a Flutter app which uses Firebase-storage and google-signin.
the steps I am trying to do is so simple:
1- Sign-in using Google (Done).
2- Get Current User Id (Done).
3- Use the User Id when construct the stream for the stream builder (the problem).
what I did so far is that I am using a Future to get the Current User Id,
then to inject the user Id inside the Where clause
.where('userId', isEqualTo: userId)
and this is what I end up with:
this is the part where I should create the stream:
// Get document's snapshots and return it as stream.
Future<Stream> getDataStreamSnapshots() async {
// Get current user.
final User user = await FirebaseAuth().currentUser();
String userId = user.uid;
Stream<QuerySnapshot> snapshots =
db
.collection(db)
.where("uid", isEqualTo: userId)
.snapshots();
try {
return snapshots;
} catch(e) {
print(e);
return null;
}
}
and this is the part where should I call and receive the stream,
...
children: <Widget>[
StreamBuilder<QuerySnapshot>(
stream: CALLING THE PREVIOUS FUNCTION,
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
...
}
...
But this code does not work, because I am not able to get the value that should returned by the Future? any idea?
thanks a lot

You should never have a Future<Stream>, that's double-asynchrony, which is unnecessary. Just return a Stream, and then you don't have to emit any events until you are ready to.
It's not clear what the try/catch is guarding because a return of a non-Future cannot throw. If you return a stream, just emit any error on the stream as well.
You can rewrite the code as:
Stream<QuerySnapshot> getDataStreamSnapshots() async* {
// Get current user.
final User user = await FirebaseAuth().currentUser();
String userId = user.uid;
yield* db
.collection(db)
.where("uid", isEqualTo: userId)
.snapshots();
}
An async* function is asynchronous, so you can use await. It returns a Stream, and you emit events on the stream using yield event; or yield* streamOfEvents;.

Related

How to get Firestore Data by method in Flutter

I am trying to get users name but Flutter gives this error:
The body might complete normally, causing 'null' to be returned, but the return type is a potentially non-nullable type.
Try adding either a return or a throw statement at the end.
Method:
String getUserNameFromUID(String uid) {
FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: uid)
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
return doc["name"];
});
});
}
How can I solve my problem? if I add return 0 to end of the method it always gives 0.
It always gives 0.(I do not want 0, I want get user name from uid)
String getUserNameFromUID(String uid) {
FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: uid)
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
return doc["name"];
});
});
return "0";
}
EDIT: I need a String solution, not Future. The method should return String...
Because my UI is not future builder. Isn't there any way to return one data as String in Firestore database?
First your function should return a Future<String> since it relies on firestore's get wich also returns a future. Also docs is a list, you have to return just one. The first one i guess. In the UI just use a FutureBuilder
Future<String> getUserNameFromUID(String uid) async {
final snapshot = await FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: uid)
.get();
return snapshot.docs.first['name'];
}
Since you can't use FutureBuilder. An ugly alternative is to pass a callback to getUserNameFromUID and call setState from there.
void getUserNameFromUID(String uid, Function (String name) onData) {
final snapshot = await FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: uid)
.get().then((s) => onData(s.docs.first['name']));
}
On your UI
...
getUserNameFromUID(uid, (String name){
setState(()=> name = name);
});
From your last comment just inherit from StatefulWidget. And call the function from inside.
#override
void initState() {
getUserNameFromUID(uid);
}
If you had special requirements about not being able to modify the UI, you should mention that as it conditions the way to use the backend services.

Download Data from firebase in flutter

I want to build a contactScreen for my flutter app. Therefor I have to download an array from Firebase. I am just able to download directly into a listView in flutter and get stuck while coding. Heres my code:
var currentUser = FirebaseAuth.instance.currentUser!.uid;
var contacts;
getUserData() async {
var userData = await FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: currentUser)
.get();
contacts = userData['contacs']; //heres the error
}
At first I want to initialize the currentUser's UID and then get the currentUser's contacts array from firebase. Therefor I build the getUserData() method to download the User and then initialize his contacts array.
The last step doesn't work in Flutter, I can't access the contacts array. Is the way I want to get the data correct?
You're at the very least missing an await before the get() call:
var userData = await FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser!.uid)
.get();
Without await your userData is of type Future<QuerySnapshot<Map<String, dynamic>>> as you get in the error message. By using await, its type will become QuerySnapshot<Map<String, dynamic>>.
you need to call await or use FutureBuilder
like this
FutureBuilder(
future: FirebaseFirestore.instance
.collection('users')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser!.uid)
.get(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [Text(snapshot.data['name'])], //error here
);
}
return Loading();
},
)

How could i retrieve data from firebse realtime database in flutter

I am saving data of different users in their uid node and in uid node i have generated different keys in which i have saved data. I my trying to retrieve email,username from keys node.
I have tried to fetch email,username using this code:-
`
#override
void initState() {
super.initState();
getCurrentUser();
rootRef.child('Manager').child(loggedInUser.uid).child(accountKey);
rootRef.once().then((DataSnapshot snap) {
var value= snap.value;
print(value['username']);
}
);
}
`
but i am getting a null value.
How could i retrieve email, username and display it to Text widget.
You need to use a FutureBuilder() since the uid will be null in the above code, therefore create a method that will return a Future<DataSnapshot>:
Future<DataSnapshot> getData() async{
var user = await FirebaseAuth.instance.currentUser();
final dbRef = FirebaseDatabase.instance.reference().child('Manager').child(user.uid).child(accountKey);
return await dbRef.once();
}
Then use it inside FutureBuilder:
FutureBuilder(
future: getData(),
builder: (context, AsyncSnapshot<DataSnapshot> snapshot) {
if (snapshot.hasData) {

Flutter - Receive and then modify data from Stream

I'm attempting to do the following:
Listen to a Firestore stream so when a new document is added, the StreamBuilder will receive it, modify it, and then present it.
The "modification" takes the Stream data, which includes a Firestore UID, gets the data from Firestore with that UID, and then the StreamBuilder is populated with that data.
So the flow is: New document added -> Stream gets document -> Function gets UID from that document -> Function uses that UID to get more data from Firestore -> Function returns to populate StreamBuilder with that new data.
My current set-up is as follows -- which works, but the FutureBuilder is obviously making the Firestore call each time the widget is rebuilt, and nobody wants that.
Stream<QuerySnapshot> upperStream;
void initState() {
super.initState();
upperStream = aStream();
}
Stream<QuerySnapshot> aStream() {
return Firestore.instance
.collection('FirstLevel')
.document(/*ownUID (not related to stream)*/)
.collection('SecondLevel')
.snapshots();
}
Future<List> processStream(List streamData) async {
List futureData = List();
for (var doc in streamData) {
Map<String, dynamic> dataToReturn = Map<String, dynamic>();
DocumentSnapshot userDoc = await Firestore.instance
.collection('FirstLevel')
.document(/*OTHER USER'S UID FROM STREAM*/)
.get();
dataToReturn['i'] = userDoc['i'];
futureData.add(dataToReturn);
}
return futureData;
}
...
...
//The actual widget
Expanded(
child: StreamBuilder(
stream: upperStream,
builder: (context, snapshot) {
// Error/null handling
return FutureBuilder(
future: processStream(snapshot.data.documents),
builder: (context, futureSnap) {
// Error/null handling
return ListView.builder(
shrinkWrap: true,
itemCount: futureSnap.data.length,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
//Continuing with populating
});
});
}),
),
What's the best way to handle a flow like this? Creating a method where the data from the Firestore stream is modified and then returned without needing ListView.builder at all?
Edit: I tried creating my own stream like this:
Stream<Map<String, dynamic>> aStream2() async* {
QuerySnapshot snap = await Firestore.instance
.collection(FirstLevel)
.document(/*OWN UID*/)
.collection(SecondLevel)
.getDocuments();
for (var doc in snap.documents) {
Map<String, dynamic> data = Map<String, dynamic>();
DocumentSnapshot userDoc = await Firestore.instance
.collection(FirstLevel)
.document(/*OTHER USER'S UID RECEIVED FROM STREAM*/)
.get();
data['i'] = userDoc['i'];
yield data;
}
}
However, the Stream is not triggered/updated when a new Document is added to the SecondLevel collection.
Alright I think I found the path to the solution. I get the data from the stream, modify it, and then yield it to the StreamBuilder within one method and no longer need the FutureBuilder. The key to this, as Christopher Moore mentioned in the comment, is await for. The stream method looks like this:
Stream<List> aStream() async* {
List dataToReturn = List();
Stream<QuerySnapshot> stream = Firestore.instance
.collection(LevelOne)
.document(OWN UID)
.collection(LevelTwo)
.snapshots();
await for (QuerySnapshot q in stream){
for (var doc in q.documents) {
Map<String, dynamic> dataMap= Map<String, dynamic>();
DocumentSnapshot userDoc = await Firestore.instance
.collection('UserData')
.document(doc['other user data var'])
.get();
dataMap['i'] = userDoc['i'];
//...//
dataToReturn.add(dataMap);
}
yield dataToReturn;
}
}
And then the StreamBuilder is populated with the modified data as I desired.
I found myself using this to implement a chat system using the Dash Chat package in my app. I think using the map function on a stream may be a little cleaner here is a sample:
Stream<List<ChatMessage>> getMessagesForConnection(
String connectionId) {
return _db
.collection('connections')
.doc(connectionId)
.collection('messages')
.snapshots()
.map<List<ChatMessage>>((event) {
List<ChatMessage> messages = [];
for (var doc in event.docs) {
try {
messages.add(ChatMessage.fromJson(doc.data()));
} catch (e, stacktrace) {
// do something with the error
}
}
return messages;
});}

Checking if TWO keys already exist in flutter Firestore using a future

I am currently using a future to check for a specific key in my flutter firestore databse, which I saw from a different answer on here:
Future<bool> doesPersonAlreadyExist(String name, DocumentSnapshot document) async {
final QuerySnapshot result = await Firestore.instance
.collection('users')
.document(currentUserId)
.collection('dislikes')
.where('nopeId', isEqualTo: document['id'])
.limit(1)
.getDocuments();
final List<DocumentSnapshot> documents = result.documents;
return documents.length == 1;
}
I can then return a futurebuilder to return different kind of widgets depending on the result of the doesPersonAlreadyExist() future:
return FutureBuilder(
future: doesPersonAlreadyExist(currentUserId, document),
builder: (context, AsyncSnapshot<bool> result) {
if (!result.hasData)
//return empty container
if (result.data)
//return empty container
else
//return as normal
I want to check in two different collections in the Future doesPersonAlreadyExist. Right now I am only checking in a collection called 'dislikes', but I also want to check in a collection called 'likes'. Is this possible with my current method, and if not, how so?
Thanks in advance.

Resources