Flutter Firestore: FirestoreBuilder with initial data - firebase

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.

Related

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.

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)

Firestore collection query as stream in flutter

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;
}),
),
);
}
}

Why is print(snapshot.hasData) returning true?

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.

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