StreamBuilder returns nothing in the snapshot - firebase

I have set a stream from database that I created manually on firebase with some sample fields, when I use streamBuilder inside a stateful widget the snapshot.data return nothing/null.
final CollectionReference **storeCollection** = Firestore.instance.collection('stores');
Stream**<List<Stores>>** get **stores** {
return **storeCollection.snapshots()**.map(_storeListFromSnapshot);
Then after I used a StreamBuilder to get snapshot.data but it returns null
#override
Widget build(BuildContext context) {
return **StreamBuilder**<Object>(
stream: DatabaseService().**stores**,
builder: (context, **snapshot**) {
**List<Stores> stores** = **snapshot.data** ?? []; //returns null on this line
I was able to update data to firebase with storeCollection.document(uid).setData()

Usually an AsyncSnapshot is received at the beginning of the listening to signal that the subscription is active and it's waiting for something and it does not contain data.
You can find more checking out the docs about the ConnectionState enum which denotes the state of an AsyncSnapshot.
That said, the most basic thing you can do with the builder of your StreamBuilder is this:
builder: (context, snapshot){
if (snapshot.hasData){
List<Stores> stores = snapshot.data;
//the rest of your logic on stores
}else{ //nothing yet, we're waiting for the event, show a loader or whatever
return Center(child: CircularProgressIndicator());
}
}

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.

How to get Metadata for Asynchronous Snapshot in Flutter

So I am pulling data from Cloud Firestore but a bit stuck on checking to see whether the data is being retrieved via the cache or server. So to do this here is how I am pulling the data from cloud firestore
marketplacedata() async {
try {
var snapshot = await FirebaseFirestore.instance
.collection('marketplaces')
.doc('All')
.collection('offers')
.get();
I'm pulling the data from the init
class _SearchMarketplaceState extends State<SearchMarketplace> {
void initState() {
widget.futuredata = getData();
super.initState();
}
getData() async {
return await FireStoreData().marketplacedata('All');
}
Then I am using future builder to retrieve the data as such
FutureBuilder(
future: widget.futuredata,
builder: (BuildContext context, AsyncSnapshot snapshot) {
var marketplacedata = snapshot.data;
if (snapshot.hasError) {
return Text('something went wrong');
}
**if (snapshot.hasData) {
HOW DO I CHECK WHETHER THE DATA IS COMING FROM CACHE?);
.metadata doesnt work on AsyncSnapShot
}**
if (searchController.text.isNotEmpty) {
marketplacedata = searchFilter(
searchController.text.toLowerCase(), marketplacedata);
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Loading();
} else {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (4 / 4),
),
itemCount: marketplacedata.length ?? 1,
itemBuilder: (BuildContext context, int index) {
return buildMarketplace(context, index,
marketplaceID: marketplacedata[index].marketplaceID,
heading: marketplacedata[index].heading,
companyDesc: marketplacedata[index].companyDesc,
iconURL: marketplacedata[index].iconURL,
keywords: marketplacedata[index].keywords,
preferred: marketplacedata[index].preferred,
imgURL: marketplacedata[index].imgURL);
},
);
}
},
),
Any help is appreciated. In the end I am trying to minimize the number of reads I am getting and hoping to get most of the data read from the cache. However I cant seem to access any data. Thanks in advance.
You can use the data property of the snapshot in your FutureBuilder to access the metadata property:
snapshot.data?.metadata.isFromCache
However, since your overall goal is to reduce backend calls, your get() call to Firestore will always fetch from the server first. See the default value of source in the GetOptions argument to get():
Source.serverAndCache (default value), causes Firestore to try to retrieve an up-to-date (server-retrieved) snapshot, but fall back to returning cached data if the server can't be reached.
You could conditionally use your Future to fetch from the Firestore cache, and subsequently from the server if there is no cached data:
Future<DocumentSnapshot> getData(Source dataSource) {
return FirebaseFirestore.instance
.collection("users")
.doc("testUser1")
.get(GetOptions(source: dataSource)); //calls using GetOptions with Source parameter
}
//using a nested FutureBuilder
return FutureBuilder<DocumentSnapshot>(
future: getData(Source.cache), //first restricts to local cache
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> cacheSnap) {
if (cacheSnap.hasError ||
!cacheSnap.hasData ||
!cacheSnap.data!.exists) {
return FutureBuilder(
future: getData(Source.server), //nested Future uses the server
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> serverSnap) {
if (serverSnap.connectionState == ConnectionState.done) {
return Text(
"Server Future: ${serverSnap.data?.metadata.isFromCache}");
}
return Text("Loading");
});
}
if (cacheSnap.connectionState == ConnectionState.done) {
return Text("Cache Future: ${cacheSnap.data?.metadata.isFromCache}");
}
return Text("loading");
},
);
If you want to listen to real time updates, then the initial state can come from the cache:
If there is a state available in a local cache, the query snapshot will be initially populated with the cached data, then updated with the server's data when the client has caught up with the server's state.
In Flutter, you would use a StreamBuilder instead of a FutureBuilder if you want to go this route. I confirmed this behavior in my project.

How to show Flutter firebase realtime database on widgets with data updates using Stream builder?

I am using Firebase for first time. My app is connected to firebase successfully and i am able access data on screen by following code. My problem is that while Values changes in database it should be changed on the screen without any button press or something.
stream: FirebaseDatabase.instance.ref("app3-e0228-default-rtdb").onvalue,
while i use above line the data on the screen will not accessible. what should I do?
StreamBuilder(
stream: FirebaseDatabase.instance.ref("app3-e0228-default-rtdb").once().asStream(),
builder: (context,AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
DatabaseEvent databaseEvent = snapshot.data!; // 👈 Get the DatabaseEvent from the AsyncSnapshot
var databaseSnapshot = databaseEvent.snapshot; // 👈 Get the DataSnapshot from the DatabaseEvent
print('Snapshot: ${databaseSnapshot.value}');
return Text("${databaseSnapshot.value}");
} else {
return CircularProgressIndicator();
}
}),
DataBase
The first problem I spot is here:
stream: FirebaseDatabase.instance
.ref("app3-e0228-default-rtdb")
.once()
.asStream(),
You're calling once(), which means you read the value from the database once (hence its name), and then turn that into a stream. So your stream will just have one element.
If you want to listen to the database and receive updates too, use onValue:
stream: FirebaseDatabase.instance
.ref("app3-e0228-default-rtdb")
.onValue
Inside your builder you'll then have a DataSnapshot that contains the data from your database, and you can simply do:
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var databaseEvent = snapshot.data!; // 👈 Get the DatabaseEvent from the AsyncSnapshot
var databaseSnapshot = databaseEvent.snapshot; // 👈 Get the DataSnapshot from the DatabaseEvent
print('Snapshot: ${databaseSnapshot.value}');
return LiveMatch();
} else {
return CircularProgressIndicator();
}
Have you implemented streamSubscription and tried listening to it?
StreamBuilder will automatically update data whenever there are changes made.

Error getting data from subcollection of flutter firebase

In a flutter messaging app, chatrooms are created and on the conversation screen, I can access the subcollection of the messages. But when the same subcollection I am trying to access on the main page
(where existing chats are shown) I cannot access them.
I have a collection of ChatRooms, in which users as an array are stored. Then, Messages named subcollection stores the messages.
See, the document is named lexicographically attached with and in between. Further, it has Messages collection.
And messages collection is also not empty.
On the main page, the existing chats are shown in listTile. I want to show the last message in its subtitle.
So, here is the last message stateful widget.
class _LastMessageState extends State<LastMessage> {
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance
.collection("ChatRooms")
.doc(widget.chatRoomID)
.collection("Messages")
.orderBy("Time")
.snapshots()
.last,
builder: (context, documentSnapshot) {
return Text(documentSnapshot.data.docs.last.get("Message"));
});
}
}
Always the bad state error is coming up.
I would be glad if you could figure out the problem.
Edit :
This is my firestore rules.
You should use the limit() method, in order to get a QuerySnapshot with only one document. Then you can do as follows, knowing the first (and unique) element of the docs list is the document you are looking for:
return FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance.
.collection("ChatRooms")
.doc(widget.chatRoomID)
.collection("Messages")
.orderBy("Time", descending: true)
.limit(1)
.get(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text("...");
}
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data.size > 0) {
return Text(snapshot.data.docs[0].get("Message"));
} else {
return Text("No document");
}
}
return Text("Loading");
},
);

Flutter Future:A build function returned null

I want to retrieve data from firebase to future builder.I use this function for that.
Future getAllData() async {
ReseviorDataModel resModel;
DatabaseReference ref = FirebaseDatabase.instance.reference();
DataSnapshot childed =await
ref.child("Reservoir/${widget.placeName}").once();
Map<dynamic, dynamic> values;
values = childed.value;
resModel = ReseviorDataModel.customConstrcutor(values["sensor1"],
values["sensor2"], values["sensor3"]);
return resModel;
}
I invoke this function inside my future builder.
child: FutureBuilder(
future: getAllData(),
builder: (context, snapshot) {
Center(child: Text(snapshot.data));
}
but it keeps throwing this "A build function returned null." error .i cant figure out what is the problem here
To clear things up here, your builder always has to return a widget to display. You can never return null, as the error message describes.
The problem in this case was that the OP didn't return anything from the builder, so just adding a return worked out fine.
Some things to keep in mind when using FutureBuilder. Always check the snapshot.hasData property and return a UI accordingly. This will prevent cases where the snapshot.data is null causing a new error to be thrown in your Widget taking in the null.
Example:
child: FutureBuilder(
future: getAllData(),
builder: (context, snapshot) {
if(!snapshot.hasData) {
// show loading while waiting for real data
return CircularProgressIndicator();
}
return Center(child: Text(snapshot.data));
}

Resources