Firestore collection query as stream in flutter - firebase

I'm trying to query a few documents from a collection, this query should listen to changes made in the queried documents, so I'd need a stream. I'm doing following (in Dart/Flutter)
Stream<List<MatchRequest>> _getNewMatches() {
return Collection<MatchRequest>(path: 'requests')
.ref
.where('status', isNull: true)
.where('users', arrayContains: ['$currentUid'])
.orderBy('last_activity')
.snapshots()
.map((list) => list.documents.map(
(doc) => Global.models[MatchRequest](doc.data) as MatchRequest));
}
(The object Collection sets the path to the ref in it's constructor, eg: ref = db.collection($path) and the map makes a model of the results)
Then I'm using a StreamBuilder with stream invoking the method above and builder checking if snapshot.hasData. But it keeps loading, snapshot.hasData keeps being false. What am I doing wrong here?
EDIT:
My firestore security rules contain:
match /requests/{requestId} {
allow read: if isLoggedIn();
allow write: if isLoggedIn();
}
When removing every where and orderBy, it doesn't find anything as well. And there are documents present in the requests-collection
When trying to query only 1 document as a stream from the requests-collection, he does find the result
Is it because I should add indexes to my firestore indexes? But this won't solve my first problem which is that even without where and orderBy, it doesn't get any data

I've written a simple example of it seems to be like what you are trying to do but are missing the listen() method:
Firestore.instance.collection('collection')
.where('field', isEqualTo: 'value')
.orderBy('field')
.snapshots()
.listen((QuerySnapshot querySnapshot){
querySnapshot.documents.forEach((document) => print(document));
}
);
This is just an example of how you can take the data from a Firestore Stream and use it on a StreamBuilder:
class _MyHomePageState extends State<MyHomePage> {
Stream dataList;
#override
void initState() {
dataList = Firestore.instance.collection('collection')
.where('field', isEqualTo: 'value')
.orderBy('field')
.snapshots();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: StreamBuilder(
stream: dataList,
builder: (context, asyncSnapshot) {
if(asyncSnapshot.hasError)
return Text('Error: ${asyncSnapshot.error}');
switch (asyncSnapshot.connectionState) {
case ConnectionState.none: return Text('No data');
case ConnectionState.waiting: return Text('Awaiting...');
case ConnectionState.active:
return ListView(
children: asyncSnapshot.data.map((document) => Text(document['value'])),
);
break;
case ConnectionState.done: return ListView(
children: asyncSnapshot.data.map((document) => Text(document['value'])),
);
break;
}
return null;
}),
),
);
}
}

Related

Flutter Firestore: FirestoreBuilder with initial data

I'm making my first Flutter app and I encounter a problem and doesn't found any solution for it.
I have a view where I render a Firestore document, and there is two ways of getting there:
From a list where I already loaded my documents
From Dynamic Links with uid attached as arguments (args)
So in order to listen document changes and loading the data when arriving from the link I used FirestoreBuilder like this:
return FirestoreBuilder<EventDocumentSnapshot>(
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<EventDocumentSnapshot> snapshot, Widget? child) {
if (!snapshot.hasData) {
return Container();
}
Event? event = snapshot.requireData.data;
return Scafold(); //Rest of my rendering code
}
);
How I could avoid first call to Firebase when I already have the data but still listen to changes? The main problem is that my hero animation doesn't work because of this.
I tried with a StreamBuilder and initialDataparam but since it's expecting stream I didn't know how to cast my data.
Okay, so I found the solution myself after many tries, so I added my Model object that can be null as initialData, but the thing that makes me struggle with is how you get the data in the builder. You have to call different methods depending on where the data is coming from.
return StreamBuilder(
initialData: args.event
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
// Here is the trick, when data is coming from initialData you only
// need to call requireData to get your Model
Event event = snapshot.requireData is EventDocumentSnapshot ? snapshot.requireData.data : snapshot.requireData;
return Scafold(); //Rest of my rendering code
}
);
Reading through cloud_firestore's documentation you can see that a Stream from a Query can be obtained via snapshots()
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
},
);
This won't help you, but with GetX it's simple to implement like this: You don't need StreamBuilder anymore.
//GetXcontroller
class pageController extends GetXcontroller {
...
RxList<EventModel> events = RxList<EventModel>([]);
Stream<List<EventModel>> eventStream(User? firebaseUser) =>
FirebaseFirestore.instance
.collection('events')
.snapshots()
.map((query) =>
query.docs.map((item) => UserModel.fromMap(item)).toList());
#override
void onReady() async {
super.onReady();
events.bindStream(
eventStream(controller.firebaseUser)); // subscribe any change of events collection
}
#override
onClose() {
super.onClose();
events.close(); //close rxObject to stop stream
}
...
}
You can use document snapshots on StreamBuilder.stream. You might want to abstract the call to firebase and map it to an entity you defined.
MyEntity fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snap) {
final data = snap.data()!;
return MyEntity (
id: snap.id,
name: data['name'],
);
}
Stream<MyEntity> streamEntity(String uid) {
return firebaseCollection
.doc(uid)
.snapshots()
.map((snapshot) => fromSnapshot(snapshot));
}
return StreamBuilder<MyEntity>(
// you can use firebaseCollection.doc(uid).snapshots() directly
stream: streamEntity(args.uid),
builder: (context, snapshot) {
if (snapshot.hasData) {
// do something with snapshot.data
return Scaffold(...);
} else {
// e.g. return progress indicator if there is no data
return Center(child: CircularProgressIndicator());
}
},
);
For more complex data models you might want to look at simple state management or patterns such as BLoC.

flutter firebase get string from database using future

I want to get a string from my DB in Firebase, I'm very confused and I don't know how to do that!
I made a big search in the few past days about this idea but unf I don't get any useful result
what do I want? I want to make a Method that returns the 'Question' string.
DB:Collection / History/question
thank you for your time
the incorrect code :
Future loadData() async {
await Firebase.initializeApp();
if (snapshot.hasError) {
return Scaffold(
body: Center(
child: Text("Error: ${snapshot.error}"),
),
);
}
// Collection Data ready to display
if (snapshot.connectionState == ConnectionState.done) {
// Display the data inside a list view
return snapshot.data.docs.map(
(document) {
return method(
document.data()['question'].toString().toString(),
); //Center(
},
);
}
}
Here is the official documentation from Flutter Fire - https://firebase.flutter.dev/docs/firestore/usage/
Read data from Cloud firestore
Cloud Firestore gives you the ability to read the value of a collection or a document. This can be a one-time read or provided by real-time updates when the data within a query changes.
One-time Read
To read a collection or document once, call the Query.get or DocumentReference.get methods. In the below example a FutureBuilder is used to help manage the state of the request:
class GetUserName extends StatelessWidget {
final String documentId;
GetUserName(this.documentId);
#override
Widget build(BuildContext context) {
CollectionReference users = FirebaseFirestore.instance.collection('users');
return FutureBuilder<DocumentSnapshot>(
future: users.doc(documentId).get(),
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.hasData && !snapshot.data.exists) {
return Text("Document does not exist");
}
if (snapshot.connectionState == ConnectionState.done) {
Map<String, dynamic> data = snapshot.data.data();
return Text("Full Name: ${data['full_name']} ${data['last_name']}");
}
return Text("loading");
},
);
}
}
To learn more about reading data whilst offline, view the Access Data Offline documentation.
Realtime changes
FlutterFire provides support for dealing with real-time changes to collections and documents. A new event is provided on the initial request, and any subsequent changes to collection/document whenever a change occurs (modification, deleted, or added).
Both the CollectionReference & DocumentReference provide a snapshots() method which returns a Stream:
Stream collectionStream = FirebaseFirestore.instance.collection('users').snapshots();
Stream documentStream = FirebaseFirestore.instance.collection('users').doc('ABC123').snapshots();
Once returned, you can subscribe to updates via the listen() method. The below example uses a StreamBuilder which helps automatically manage the streams state and disposal of the stream when it's no longer used within your app:
class UserInformation extends StatefulWidget {
#override
_UserInformationState createState() => _UserInformationState();
}
class _UserInformationState extends State<UserInformation> {
final Stream<QuerySnapshot> _usersStream = FirebaseFirestore.instance.collection('users').snapshots();
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return new ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document.data()['full_name']),
subtitle: new Text(document.data()['company']),
);
}).toList(),
);
},
);
}
}
By default, listeners do not update if there is a change that only affects the metadata. If you want to receive events when the document or query metadata changes, you can pass includeMetadataChanges to the snapshots method:
FirebaseFirestore.instance
.collection('users')
.snapshots(includeMetadataChanges: true)

Merging multiple firebase stream in flutter

Guy this is a real problem merging multiple firebase streams to one stream. Someone should write an article or a simple video tutorial on this. Either using StreamGroup, FlatMap(), Rx.combineLatest, StreamZip or CombineLatestesStream. I have tried solving this since yesterday and I cant get a clear guidance.
class CartPage extends StatefulWidget{
#override
_CartPageState createState() => _CartPageState();
}
class _CartPageState extends State<CartPage> {
// a firebase collection for all items
Stream stream1 = EcommerceApp.firestore
.collection("items")
.where("shortInfo",
whereIn: EcommerceApp.sharedPreferences
.getStringList(EcommerceApp.userCartList))
.snapshots();
// a firebase collection for flash sales items
Stream stream2 = EcommerceApp.firestore
.collection("flashitem")
.where("shortInfo",
whereIn: EcommerceApp.sharedPreferences
.getStringList(EcommerceApp.userCartList))
.snapshots();
List<QuerySnapshot> getList(QuerySnapshot list1) {
List<QuerySnapshot> result = [];
(list1 as List).forEach((element) {
result.add(element);
});
return result;
}
#override
Widget build(BuildContext context) {
Stream combineStream = Rx.combineLatest2(streamA, streamB, (a, b) => [a, b]);
return Scaffold(
appBar: MyAppBar(),
body:CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
height: 10.0,
),
),
StreamBulder(
stream: combineStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SliverToBoxAdapter(
child: Center(
child: circularProgressBar(),
),
);
} else {
List<QuerySnapshot> _list = [];
_list.addAll(getList(snapshot.data[0]));
_list.addAll(getList(snapshot.data[1]));
if (_list.length == 0) {
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
ProductModel model = ProductModel.fromJson(
_list[index].docs[index].data());
return cartSourceInfo(model, context,
removeCartFunction: () =>
removeItemFromUserCart(model.shortInfo));
},
childCount: childCount: snapshot.hasData ? _list.length : 0,
),
);
}
}
}
)
);
}
}
Majority of the answers here are using Observable library which is deplecated in rxdart, and when am trying to use the same syntax to solve using Rx.latestCombine2 there is no data streamed. and when I try to pass a querySnapshot of type list to a stream Stream<List> I am getting a batch of errors:
Class 'List' has no instance getter 'docs'.
Receiver: Instance(length:2) of '_GrowableList'
Tried calling: docs
Please show me how I can either nest these two firebase stream into ome or how I can use Rx.combineLatest2 method to solve this problem.
The syntax looks correct but when trying to access data of each stream you have to access it by index since the snapshot is basically a list
so to access snapshot of stream1 and stream2 it should be accesssed like this
snapshot.data[0].docs and snapshot.data[1].docs respectively.
You can combine both the streams and show the list in the Ui, And make sure to assign a appropriate type T based on the type of snapshot.data[index].docs
List<QuerySnapshot> combineLists(
List<QuerySnapshot> list1, List<QuerySnapshot> list2) {
List<QuerySnapshot> result = [];
list1.forEach((element) {
result.add(element);
});
list2.forEach((element) {
result.add(element);
});
return result;
}
StreamBulder(
​stream: combineStream,
​builder: (context, AsyncSnapshot<List<QuerySnapshot>> snapshot) {
​if (!snapshot.hasData) {
​return SliverToBoxAdapter(
​child: Center(
​child: circularProgressBar(),
​),
​);
​} else {​
final List<QuerySnapshot> _list=[];
final List<QuerySnapshot> combineSnapshot =
combineLists(snapshot.data[0], snapshot.data[1]);
​if (_list.length == 0) {
​return addItems();
​} else {
return SliverList(
​delegate: SliverChildBuilderDelegate(
(context, index) {
ProductModel model = ProductModel.fromJson(
_list[index].data());
return cartSourceInfo(model, context, removeCartFunction:
() => removeItemFromUserCart(model.shortInfo));
​},
childCount:_list.length,
),
);
​}
}
}

Flutter /Firebase - Accessing the realtime database

In my flutter project, I need to access the realtime database to get the name of the user when they create a post. This is the code I'm working with:
class PostScreen extends StatelessWidget {
static const String idScreen = 'post';
final String name1 = FirebaseDatabase.instance
.reference()
.child('users')
.child(FirebaseAuth.instance.currentUser.uid)
.child('name1')
.toString();
final String name2 = FirebaseDatabase.instance
.reference()
.child('users')
.child(FirebaseAuth.instance.currentUser.uid)
.child('name2')
.toString();
#override
Widget build(BuildContext context) {
return Scaffold(
body: FlatButton(
child: Text('Create Post'),
onPressed: () {
MainScreen.posts.add(Post(
name1: name1,
name2: name2,
));
Navigator.pushNamed(context, MainScreen.idScreen);
},
),
);
}
}
class Post extends StatelessWidget {
String name1 = '';
String name2 = '';
Post({#required name1, #required name2});
#override
Widget build(BuildContext context) {
return Card(
child: Row(
children: [
Text(name1),
Text(" and "),
Text(name2),
],
),
);
}
}
What happens though is that the name is left blank and just creates a card that says " and ". What could I be doing wrong?
Your code doesn't read anything from the database yet. For that to happen you need to call the once() stream, or listen to the onValue or onChild... streams.
I also recommend simplifying your problem before continuing. So instead of reading the data for the current user (which requires that you have a current user), simply write some hard-coded data at a known location in the database and read that first. That should look something like this:
final DatabaseReference ref = FirebaseDatabase.instance
.reference()
.child('test');
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: ref.onValue,
builder: (BuildContext context, snapshot) {
if(snapshot.hasData) => return Text(snapshot.value);
else if(snapshot.hasError) => return Text("Error");
else return Text("No data (yet)");
}
);
}
There may be syntax errors in this code, so treat it as an example of an approach instead of a copy/paste solution please. If you find any of such errors, please try to solve them on your own - and edit the answer with any fixes.
Also see:
How To Use Firebase RTDB with Flutter Stream
more of these search results
You should be able to do something like this:
DatabaseReference myRef = FirebaseDatabase.instance
.reference()
.child('users')
.child(FirebaseAuth.instance.currentUser.uid)
.child('name1');
StreamBuilder(
stream: myRef.onValue,
builder: (context, AsyncSnapshot<Event> snap) {
if (snap.hasData && !snap.hasError && snap.data.snapshot.value != null) {
// Handle snapshot data
}
}
If you don't need to continue getting changes from the location you can probably use a future builder and .once() method. Don't have experience with that myself though.

Nested Futures for FutureBuilder

I have a FutureBuilder widget that should wait for data from a firestore collection.
class MyScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: calendarQuery.getCalendarEntry(dateString),
builder: (BuildContext context, AsyncSnapshot snap) {
if (snap.hasData) {
List<Events> recipe = snap.data;
return Scaffold(
appBar: AppBar(
title: Text("Events"),
),
body: Column(
children: <Widget>[
...,
],
),
);
} else {
return LoadingScreen();
}
},
);
}
}
I retrieve a list of events and then for each event I need to fetch some additional details. I tried to do nested Futures and came up with the code below. It generates a Future<Iterable<Future<Detail>>> which ends up as MappedListIterable<DocumentSnapshot,Future<Recipe>> in snap.data and i cannot handle it.
class CalendarQuery<T> {
...
Future<Iterable<Future<Detail>>> getCalendarEntry(String date, String type) async {
return await ref
.where("date", isEqualTo: date)
.getDocuments()
.then((data) {
return data.documents.map((doc) => Document<Details>(path: 'detailCollection/${doc.data["Event"]["SomeId"]}')
.getData());
});
}
}
I think I went wrong here at some points with handling the futures and there is probably a proper way to do this.
Does someone know a way to refactor getCalendarEntry so that it returns a Future<List<T>>? Or maybe there is a better approach to solve this?
You can use Future.wait to create a Future that completes once all Futures from an Iterable have completed.
In your scenario, you would replace return await ref with return Future.wait(ref and a closing bracket where needed, to create a Future that waits for all Details to be retrieved.

Resources