I must be misunderstanding the hasData method for a QuerySnaphot. In my StreamBuilder I want to return a widget informing the user there are no items in the collection queried. I've deleted the collection in Firestore so there's definitely no data there. But when I run the following code:
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('Events')
.where("bandId", isEqualTo: identifier)
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
print('code here is being executed 1');// This gets executed
return Text('helllllp');
} else {
print('Code here is being executed2'); //And this gets executed
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Text('Loading...');
default:
return new ListView(
children:
snapshot.data.documents.map((DocumentSnapshot document) {
return CustomCard(
event: document['event'],
location: document['location'],
service: document['service'],
date: document['date'].toDate(),
);
}).toList(),
);
}
}
},
),
All I want to do is return a widget informing user if the snapshot is empty. For example Text('You have no messages')
The problem here is that snapshots() will also return a QuerySnapshot when the query returns no documents. Thus, you could expand your condition like this:
if (!snapshot.hasData || snapshot.data.documents.isEmpty) {
return Text('You have no messages.');
} else {
...
}
Although, realistically you should not return You have no messages when snapshot.data is null because it is null before the query is completed. Hence, I would go for something like this:
if (!snapshot.hasData) {
return Text('Loading...');
}
if (snapshot.data.documents.isEmpty) {
return Text('You have no messages.');
}
return ListView(..);
This ignores error handling, however, that can also be added.
Notice that snapshot.hasData is an alternative to determining connection state using snapshot.connectionState.
Related
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.
I wrote this code which queries Firebase database and returns some data. This thing was working totally fine till few days back but i don't know why now it returns "no data" even if i have data in the database
Is there a error in the code or Something else cause this ?
Fetching the data from Firebase
getsearchfood(String query) {
Future<QuerySnapshot> search =
Firestore.instance.collection("data").getDocuments();
setState(() {
searched = search;
});
}
Operating the data
buildsearchfood() {
return FutureBuilder(
future: searched,
builder: (context, snapshot) {
if (snapshot.hasData) {
print("no data");
}
else{
print("present data");
}
return Container(
);
},
);
}
Add await, as it is an asynchronous operation
Future<QuerySnapshot> search =
await Firestore.instance.collection("data").getDocuments();
You don't need to setState like you did.
Try this:
buildsearchfood() {
return FutureBuilder(
future: Firestore.instance.collection("data").getDocuments(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// This means you have data. You can reference it in `snapshot.data`
}
else{
// This means NO data
}
},
);
}
StreamBuilder(
stream: Firestore.instance.collection('aaaaaaaaaaaaaaaa').snapshots(),
builder: ( context , snap){
if(snap.data == null){
print('A');
}
if(snap.data != null){
print(snap.data);
print('B');
}
Obviously collection ('aaaaaaaaaaaaaa') does not even exists then why it is not returning null?
Querying a non-existing (or empty) collection is not an error. So when there are no results, Firestore gives you an empty QuerySnapshot, instead of an error.
To detect if there were any results, check if the QuerySnapshot has any documents (FlutterFire doesn't seem to wrap the native QuerySnapshot.isEmpty() method.
You should use hasData to check if there is data or not:
StreamBuilder(
stream: Firestore.instance.collection('aaaaaaaaaaaaaaaa').snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
print("a");
} else{
return Text("No data");
}
return CircularProgressIndicator();
},
),
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;
}),
),
);
}
}
I am trying to make an app where I need a nested stream builder. The Stream builder looks something like this. but the widgets are built before the last stream is loaded so I get error calling getter null,
StreamBuilder(
stream: some_stream,
builder: (context, data){
return StreamBuilder(
stream: some_stream,
builder: (context, data){
return StreamBuilder(
stream: some_stream,
builder: (context, data){
return someWidget;
}
);
}
);
}
);
It is entirely possible that StreamBuilder.builder will be called while the Stream has no data, hasn't connected to the Stream yet or the value is null.
It is up to you to make sure that you handle all these cases.
To make sure that the initial value is never null, you can set inialData.
Future<String> someFutureString = Future.value('initial data seeded');
new StreamBuilder<String>(
initialData: await someFutureString,
builder: (ctx, snapshot) { /* ... */ }
);
This is bad practice though. It is better to build such builder that consider the state of the snapshot it works with. Building the widget tree should be quick. Imagine having to wait 3 seconds to the initialData. Your widget tree building will be blocked by at the first await.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
wrapInMaterialApp(Widget widget) => MaterialApp(
home: widget,
);
main() {
testWidgets('can await initial data', (WidgetTester tester) async {
final initialData = Future<String>.value('initial value');
final stream = Stream.fromIterable(['first']);
final sb = StreamBuilder<String>(
initialData: await initialData,
builder: (ctx, snapshot) {
return Text('${snapshot.data}');
},
);
await tester.pumpWidget(wrapInMaterialApp(sb));
// Verify that initial data is present
expect(find.text('initial value'), findsOneWidget);
});
testWidgets('can return subtree if there is data', (WidgetTester tester) async {
final stream = Stream.fromIterable(['first']);
final sb = StreamBuilder<String>(
stream: stream,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
return Text('${snapshot.data}');
} else
return Container();
},
);
var wrappedWidget = wrapInMaterialApp(sb);
await tester.pumpWidget(wrappedWidget);
expect(find.byType(Container), findsOneWidget);
expect(find.text('first'), findsNothing);
await tester.pump();
expect(find.byType(Container), findsNothing);
expect(find.text('first'), findsOneWidget);
});
}
Other things that can help you determine what Widget your builder should return is ConnectionState, accessible through snapshot.connectionState.
Cheers!
You should always approach the structure for the StreamBuilder like,
StreamBuilder(
stream: some_stream,
builder: (context, data){
return StreamBuilder(
stream: some_stream2,
builder: (context, data){
if(data.hasError) {
return Text("Error Occured!!");
} else if(data.hasData) {
return StreamBuilder(
stream: some_stream,
builder: (context, data){
if (data.hasError){
return Text("Error Occured!!");
} else if (data.hasData) {
return someWidget;
}else {
return CircularProgressIndicator();
}
}
);
} else {
return CircularProgressIndicator();
}
}
);
}
);
This will save you from errors for most of the time.