Flutter Streambuilder stream inconsistent FirebaseFirestore snapshot data - firebase

Retrieving shopping cart items my snapshot data is inconsistent. When one item is in cart I get this correctly formatted result:
{1111111111111: 1, PriceSmart: 540.0}
When two items in cart, and second item is also "PriceSmart", I get an error because returns this result:
{1111111111111: 1, PriceSmart: 300.0, 5555555555555: 1}
and should be:
{1111111111111: 1, PriceSmart: 540.0, 5555555555555: 1, PriceSmart: 300.0}
This is my firebase data structure:
First cart item:
Second cart item:
Basically is combining the "seller" (PriceSmart), when I need to return complete data from each cart item, otherwise I get an error as soon as I have more than one item in cart and seller is the same.
Please check the Stream in my code and see what is wrong with this implementation:
class PriceUpdaterWidget extends StatefulWidget {
const PriceUpdaterWidget({
Key? key,
required this.loginService,
required this.code,
required this.itemSubCategory,
}) : super(key: key);
final LoginService loginService;
final String? code;
final SubCategory? itemSubCategory;
_PriceUpdaterWidgetState createState() => _PriceUpdaterWidgetState();
}
class _PriceUpdaterWidgetState extends State<PriceUpdaterWidget> {
#override
Widget build(BuildContext context) {
CategorySelectionService catSelection =
Provider.of<CategorySelectionService>(context, listen: false);
Stream<DocumentSnapshot> priceDocStream = FirebaseFirestore.instance
.collection('shoppers')
.doc(widget.loginService.loggedInUserModel!.uid)
.collection("cartItems")
.doc(widget.code)
.snapshots();
return StreamBuilder<DocumentSnapshot>(
stream: priceDocStream,
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
SellerNameService isSellerName =
Provider.of<SellerNameService>(context, listen: false);
var sellerName = isSellerName.isSellerName;
if (snapshot.data != null) {
return Text(
snapshot.data![sellerName].toStringAsFixed(2),
textAlign: TextAlign.center,
);
} else {
return Text('No Data');
}
});
}
}

Without seeing more of your code, it's hard to make an example to exactly fit your specification, and the error you're getting doesn't match the code you posted, but, broadly, you need to:
move the Stream outside your build function and into initState
process each snapshot one at a time
stay away from processing the data in your build widget
class PriceUpdaterWidget extends StatefulWidget {
final String login, code;
const PriceUpdaterWidget(this.login, this.code);
#override
_PriceUpdaterWidgetState createState() => _PriceUpdaterWidgetState ();
}
class _PriceUpdaterWidgetState extends State<PriceUpdaterWidget> {
Stream<DocumentSnapshot> priceStream; // only one stream per widget
#override
void initState() {
super.initState();
priceStream = FirebaseFirestore.instance // set the stream once
.collection("shoppers")
.doc(widget.login)
.collection("cartItems")
.doc(widget.code)
.snapshots();
}
#override
Widget build(BuildContext context) => StreamBuilder<DocumentSnapshot>(
stream: priceStream,
builder: (context, snapshot) {
const String sellerName = "PriceSmart";
return snapshot.data == null
? const Text("No data")
: Text(
snapshot.data[sellerName].toStringAsFixed(2),
textAlign: TextAlign.center,
);
}
);
}

Related

show field from realtime database flutter

I'm still learning flutter and ,I want to get a value from Realtime database in firebase and then show it in the screen.
this is the full code , i can see the value in the terminal but when i dont know how to display it on the screen
class Body extends StatefulWidget {
#override
BodyState createState() => BodyState();
}
class BodyState extends State<Body> {
final db = FirebaseFirestore.instance;
late final reference = FirebaseDatabase.instance.ref();
late DatabaseReference databaseReference ;
#override
Widget build(BuildContext context) {
DatabaseReference tempvaleur =
FirebaseDatabase.instance.ref('temperature/esofostemperature0/valeur');
tempvaleur.onValue.listen((DatabaseEvent event) async {
print(event.snapshot.value);
final data = event.snapshot.value ;
}
);
return Column(
);
}
}
You should instead use the StreamBuilder which allows you to consume the real-time stream from a Firebase collection or document.
In your case, your build method would've looked like this:
#override
Widget build(BuildContext context) {
DatabaseReference tempvaleur =
FirebaseDatabase.instance.ref('temperature/esofostemperature0/valeur');
return StreamBuilder(
stream: tempvaleur.onValue,
builder: (context, snapshot) {
if (snapshot.hasData) {
// consume your data here
var data = (snapshot.data! as DatabaseEvent).snapshot.value;
// hoping your value is a string, but just in case...
return Text(data.toString());
}
return CircularProgressIndicator();
}
);

Pass fetched value to a firestore reference to flutter's streambuilder

I'm accessing a user's favorite group which is inside groupfav in Firestore, when I get it I want to give it as part of the reference to the streambuilder stream:, so that it knows what to show in a list, but I can't pass the variable that contains the favorite group, what should I do or what am I doing wrong?
static String? userID = FirebaseAuth.instance.currentUser?.uid; // get current user id
static var taskColeccion = FirebaseFirestore.instance.collection("usuarios");
var tack = taskColeccion.doc("$userID").get().then((value) {
var groupfav = value.data()!["groupfav"]; // value i get from firestore
return groupfav;
});
late Stream<QuerySnapshot> task = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("tareas")
.snapshots();
photo of firestore
The photo shows how Firestore's logic is and the value marked in green is what I must pass to the late Stream<QuerySnapshot> task... in its reference, logically it is a random value that I would not know. thanks for any help!
this is what the code looks like now (I took things that were not important)
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
static String? userID = FirebaseAuth.instance.currentUser?.uid;
static final taskColeccion =
FirebaseFirestore.instance.collection("usuarios");
String groupfav = '';
final tack = taskColeccion.doc("$userID").get().then((value) {
groupfav = value.data()!["groupfav"];
return groupfav;
});
Stream<QuerySnapshot> task = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("tareas")
.snapshots();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Home"),
automaticallyImplyLeading: false,
),
body: StreamBuilder(
stream: task,
builder: (
BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot,
) {
if (snapshot.hasError) {
return const Text("error");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("cargando");
}
final data = snapshot.requireData;
return ListView.builder(
itemCount: data.size,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text("${data.docs[index]['titulo']}"),
subtitle: Text("${data.docs[index]['contenido']}"),
onTap: () {},
trailing: IconButton(
icon: const Icon(Icons.delete),
color: Colors.red[200],
onPressed: () {
// delete function
},
),
),
);
},
);
},
),
);
}
}
You just need to declare groupfav outside of the scope of the get method of taskColeccion;
The way you have it, the variable no longer exists by the time you're trying to pass it into the task stream.
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
static String? userID = FirebaseAuth.instance.currentUser?.uid;
static final taskColeccion =
FirebaseFirestore.instance.collection("usuarios");
String groupfav = '';
late Stream<QuerySnapshot> task;
#override
void initState() {
super.initState();
taskColeccion.doc("$userID").get().then((value) {
groupfav = value.data()!["groupfav"];
return groupfav;
});
task = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("tareas")
.snapshots();
}

Flutter firestore check if document with specific id exists

I want to check if the firestore document with a specific id exists. Actually, my idea is to store and fetch user-specific data from firestore. If there is no user-specific data uploaded it will show "no data to show" in the app. Till now what I have done is adding data to firestore with document id equals to the current user's UID.
FirebaseFirestore.instance.collection("doc_folder").doc(currentuser!.uid).set(data);
now I am unable to check if the firestore database contains any document with this data. By far, I have reached:
class Crud {
getData() {
return FirebaseFirestore.instance
.collection('doc_folder')
.where("userId", isEqualTo: currentUser!.uid)
.get();
}
}
#override
void initState() {
crud.getData().then((result) {
snap = result;
setState(() {});
});
super.initState();
}
Widget build(BuildContext context){
return Container(
snap != null? //if code
: //else code
)
}
The above code returns "document exists" even if the data does not exist with the current user's UID.
The following line from your code returns a QuerySnapshot which is not nullable:
FirebaseFirestore.instance.collection('doc_folder').where("userId", isEqualTo: currentUser!.uid).get()
and you assign the returned value of QuerySnapshot to snap and perform the following:
snap != null ? ... : ...
However, this condition will be true regardless of whether a document exists.
You should instead check docs property from QuerySnapshot, which is a list of the document snapshots returned for your query.
I'd prefer the widget of FutureBuilder over calling getData() in initState for this example to make it clear:
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future: crud.getData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data.docs.isNotEmpty) {
// Document exists
} else {
// Document does not exist
}
} else {
// Show a loading widget
// ...
}
},
);
}
Full Example
class Crud {
Future<QuerySnapshot> getData() async {
return await FirebaseFirestore.instance
.collection('doc_folder')
.where("userId", isEqualTo: currentUser!.uid)
.get();
}
}
class MyPage extends StatefulWidget {
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final crud = Crud();
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future: crud.getData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final querySnaphost = snapshot.data; // Get query snapshot
if (querySnaphost.docs.isNotEmpty) {
// Document exists
final documentSnapshot =
querySnaphost.docs.first; // Get document snapshot
return Text('${documentSnapshot.data()}'); // Get the data
} else {
// Document does not exist
return Text('Document does not exist.');
}
} else {
// Show a loading widget
return CircularProgressIndicator();
}
},
);
}
}
The reason is that value of snapshot is not null even though document doesn't exists. So use below code:-
QuerySnapshot snap=await FirebaseFirestore.instance.collection('doc_folder').where("UserId", isEqualTo: currentuser!.uid).get();
if(snap.docs.isNotEmpty)
DocumentSnapshot doc=snap.docs.first;
print(doc['username']);//like this you can access data
else{
print("Doc doesn't exits");
}
You can do that
var
doc=FirebaseFirestore.instance
.collection('doc_folder')
.where("userId", isEqualTo:
currentUser!.uid)
.get();
if(doc.exist){
print('exist');
}
There is a slight problem with your code. build() is called while async crud.getData() is still running. Therefore snap will be have its default value. If snap's default value is not null, then snap != null will be true and you might assume your snap has its intended value.
Full working code
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Crud crud = Crud();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: FutureBuilder<QuerySnapshot<Map<String, dynamic>>>(
future: crud.getData(),
// above is called everytime the widget is rebuilt which is not optimal
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
if (snapshot.hasError) return Text('Something went wrong');
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
if (snapshot.data.docs.isNotEmpty) {
print('document exists');
print(snapshot.data.docs.map((e) => e.data()));
return Container();
} else {
print('document does not exist');
return Container();
}
},
),
);
}
}
class Crud {
Future<QuerySnapshot<Map<String, dynamic>>> getData() {
return FirebaseFirestore.instance
.collection('users')
.where("userId", isEqualTo: 'c1fG8zo0OWgHsPClEKWN')
.get();
}
}
FirebaseFirestore.instance
.collection('Company')
.doc('1235')
.get()
.then((value) {
if (value.exists) {
// Do something Here
}
The question asked to check if the document exists, and the answer accepted only works if docId is stored as field in that doc. Needs Review.

Firestore one-time read using Flutter, i got to printing document data in console want to output it in UI

In my Firestore DB, inside 'location' collection i have 2 docs,(named as Europe,Australia) having a field 'name' with their string values (same as their document names).
I have worked with StreamBuilder and Streams before, but this time i dont want real-time calls, but just once.
I wanna print that 'name' field data of all the docs inside location collection.
This is what my UI code looks like:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double spaceInBetween = 25;
#override
Widget build(BuildContext context) {
DatabaseService().getData();
return Scaffold(
body: Container(
child: Text("data here")
);
}
I wanna print all that documents data, with all their names using ListView.builder() on the HomePage.
This is my DatabaseService class (using the official FlutterFire Docs https://firebase.flutter.dev/docs/firestore/usage/ but didnt find what i was looking for)
class DatabaseService {
final locationCollection = FirebaseFirestore.instance.collection("location");
getData() async {
await locationCollection.get().then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
print(doc['name']);
});
});
}
}
Also wanted to know if there's any other way i could do this, using FutureBuilder or anything else, just wanna get field data from all docs in a collection from Firestore and print it (im still learning).
Thank you :)
I think the answer is FutureBuilder. You can create a Future method which is going to get datas from Firebase servers and return it. After that you just create a FutureBuilder which is going to help you to show datas and if something wrong with the server or the internet connection you will not get any error messages because FutureBuilder will show an CircularProgressIndicator.
I made a demo code for you to demostrate FutureBuilder.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final locationCollection = FirebaseFirestore.instance.collection("location");
#override
void initState() {
super.initState();
}
Future<List<String>> getData() async {
List<String> name = [];
await locationCollection.get().then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
name = doc['name'];
});
});
return name;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: FutureBuilder<List<String>>(
future: getData(), // call getData method
builder: (context, snapshot) {
List<String> nameList = snapshot.data ?? []; // create a local variable which is storing data from the getData method
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? ListView.builder( // if getData method give datas listviewbuilder is going to show datas
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(nameList[index]),
);
},
)
: Center(child: CircularProgressIndicator()); // if something wrong with the server or with the internet you will see a CircularProgressIndicator
}),
),
),
);
}
}
In order to ensure you only get the data once, you can use a FutureBuilder and ensure you define the future outside the build method (for example in the initState) so that it doesn't get called again whenever the build method is called.
FutureBuilder
...
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateWidget, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Update the getData method of your DatabaseService class to this below:
Future<List<String>> getData() async {
final QuerySnapshot locationDataSnapshot = await locationCollection.get();
final List<String> listOfNames = locationDataSnapshot.docs
.map((QueryDocumentSnapshot documentSnapshot) =>
documentSnapshot.data()['name'] as String)
.toList();
return listOfNames;
}
This code above fetches the list of documents from the location collection and maps them to a list of names, which is then returned.
You can then get define the future object to get this data in your initState and use it in your FutureBuilder like shown below:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<List<String>> _nameListFuture;
#override
void initState() {
super.initState();
_nameListFuture = DatabaseService().getData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<String>>(
future: _nameListFuture,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(child: CircularProgressIndicator());
}
final List<String> nameList = snapshot.data;
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (context, index) => Text(nameList[index]),
);
},
),
);
}
}

Flutter Firebase: How to remove firebase listener when using stream builder?

Every example about StreamBuilder starts with a StatelessWidget even in flutter example but how do you cancel the subscription in a StatelessWidget widget? For example, I was going through firestore example.
class MessageList extends StatelessWidget {
MessageList({this.firestore});
final Firestore firestore;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('messages').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
final int messageCount = snapshot.data.documents.length;
return ListView.builder(
itemCount: messageCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document = snapshot.data.documents[index];
return ListTile(
title: Text(document['message'] ?? '<No message retrieved>'),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
);
},
);
}
}
Now how do I cancel listening to firestore.collection('messages').snapshots() stream?
I use realtime database in my app and this is how I do it
class MessgaeView extends StatefulWidget {
final String _chatId;
MessgaeView(this._chatId);
#override
_MessgaeViewState createState() => _MessgaeViewState();
}
class _MessgaeViewState extends State<MessgaeView> {
Stream<Event> _messageStream;
#override
void initState() {
_messageStream = _database
.reference()
.child("message/${widget._chatId}")
.limitToLast(1)
.onChildAdded;
super.initState();
}
#override
void dispose() {
_messageStream.drain();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _messageStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CupertinoActivityIndicator();
final message =
(snapshot.data.snapshot as DataSnapshot).value['message'];
return Text(message);
});
}
}
Simply replace the previous stream instance with null.
This will require a code similar to the following somewhere:
setState(() {
_messageStream = null;
});
Doing so will stop listening to the stream. But StreamBuilder will still hold the previous value.
I hava same problem and solved by StreamSubscription
For example define StreamSubscription as global
StreamSubscription<Event> _counterSubscription;
then in your place you want to listen to data register your Subscription like this
_counterSubscription = _counterRef.onValue.listen((Event event) {
setState(() {
_counter = event.snapshot.value ?? 0;
});
});
and when you want to remove listener just make this code
if(_counterSubscription !=null)
_counterSubscription .cancel();

Resources