Flutter give Streambuilder initalData - firebase

I have a Streambuilder that takes Firebase Firestore snapshot as a Stream and I would like to add initalData to the Streambuilder.
How can I pass initalData to the Streambuilder?
The Code:
StreamBuilder(
stream: FirebaseFirestore.instance
.collection("events")
.snapshots(),
builder: (BuildContext ctx, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData || snapshot.data.docs.isEmpty) {
return NoDataRelatedLocation();
}
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return new RelatedLocationListing(
relatedLocationList: snapshot.data.docs,
);
}
},
),

You can add initialData to StreamBuilder:
StreamBuilder(
initialData: ..... // <~~~ add it here.
stream: ...
builder: ...
You just need to make sure that your initialData matches that type of data coming from the stream. Since QuerySnapshot is a Firebase specific type, you should map your stream to a data type that you can create and that's known to you.
Here's a pseudo code:
initialData: [MyDataType()],
stream: FirebaseFirestore.instance
.collection("events")
.snapshots().map((snapshot) => MyDataType.fromMap(snapshot.doc));

Related

Is there a way to other way of calling two collection in 1 stream builder?

I'm currently using stream builder and future builder to call two collections at the same time. I'm having hard time because the stream builder refreshes every time the database changes. Here's my source code:
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('thread')
.orderBy('published-time', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return snapshot.data!.docs.length > 0
? MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView(
shrinkWrap: true,
children: snapshot.data!.docs.map((DocumentSnapshot postInfo) {
return FutureBuilder<DocumentSnapshot>(
future: userCollection
.doc(postInfo.get('publisher-Id'))
.get(),
My variables are here:
final CollectionReference userCollection =
FirebaseFirestore.instance.collection('users');
final FirebaseAuth _auth = FirebaseAuth.instance;
Also tried calling two streambuilders:
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('thread')
.orderBy('published-time', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return snapshot.data!.docs.length > 0
? MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView(
shrinkWrap: true,
children: snapshot.data!.docs
.map((DocumentSnapshot postInfo) {
return StreamBuilder<DocumentSnapshot>(
stream: userCollection
.doc(postInfo.get('publisher-Id'))
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map<String, dynamic> userInfo =
snapshot.data!.data()
as Map<String, dynamic>;
It doesn't look like there is a better way of calling two collections, but you can achieve less rebuilds by considering some optiomization steps mentioned in this article:
Only wrap the widget that should rebuild during a stream change inside a StreamBuilder
Use the Stream.map to map your stream object into an object that your widget needs to show in UI.
Use the Stream.distinct to create a _DistinctStream in case your widget shouldn’t rebuild when the stream provides the same value in a
row.
Create a separate _DistinctStream for StreamBuilders on initState so that they can save streamed values first if your
streamController streams a new value before the screen's first
build.

Getting the total price of products in Firestore Flutter

This is my first time asking a question because I've tried everything that I've researched from this website for hours but nothing seems to work. I'm trying to get the total price of my cart but every time the state changes it constantly adds and I just want the total to stay as it is when I'm changing pages. Even clicking a delete button from my cart page, refreshes the function and it adds another value to the total. Thank you so much if someone could help me
Here's the code that I've tried
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: white,
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("users")
.doc(user.uid)
.collection("cart")
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
FirebaseFirestore.instance
.collection("users")
.doc(user.uid)
.collection("cart")
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) {
total += result.data()['price'];
});
});
First of all why do you send a request inside builder again? There is already a variable called "snapshot". You can get data with snapshot.data.docs.
Secondly, you are trying to increase total value every time without reset. If you set total variable to 0 before adding the prices probably it will solve your problem.
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: white,
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("users")
.doc(user.uid)
.collection("cart")
.snapshots(),
builder:(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if(snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else {
total = 0;
snapshot.data.docs.forEach((result) {
total += result.data()['price'];
});
return WidgetYouWantToUse();
}
}
Welcome!
First of all, you are using a StreamBuilder and even though you provide a Stream, you call the stream again at builder method, you can can access the value of the Stream through the snapshot. Moreover, your builder returns no Widget but just iterates through the items of the Stream, to calculate I guess the total price. Instead, you can change the builder method to something like this
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: white,
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("users")
.doc(user.uid)
.collection("cart")
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
var data = snapshot.data;
var totalCartPrice = data.fold(0,(prev, element)=>prev['price']+element['price']);
return Text(totalCartPrice);
});
Instead of using fold method you can iterate the data and do the calculations by hand. But I find it more elegant.
There was no reason in calling setState aswell, the StreamBuilder fires changes if anything changes in the Firestore document.
The problem is that total value keeps on increasing whenever stream builder is called. Also, you do not need to listen to stream and still call get(). The stream returns similar value to the get.
Change your code to this
return Scaffold(
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("users")
.doc(user.uid)
.collection("cart")
.snapshots(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasError) return Text('Something went wrong');
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
// NB: I set the value of total = 0; so that anytime the stream
// builder is called, total starts from 0.
total = 0;
snapshot.data.docs.forEach((result) {
total += result.data()['price'];
});
print(total);
print('done');
return Text('done');
},
),
);
On the other hand, you can still call this function using futureBuilder.
return Scaffold(
body: FutureBuilder(
future: FirebaseFirestore.instance
.collection("users")
.doc(user.uid)
.collection("cart")
.get(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasError) return Text('Something went wrong');
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
total = 0;
snapshot.data.docs.forEach((result) {
total += result.data()['price'];
});
print(total);
print('done');
return Text('done');
},
),
);
The difference between stream and future builder is that future builder is only called once. Stream builder is called whenever the data changes.

How to query data from Firebase with an async call to uid

I'm trying to fetch data that belong to the logged in user, however, the "getuserui" is async, for some reason. even though the user is logged in to do stuff inside the app the function still returns a Future....
I've lost count to how many different things i've tried, including .then and such but here's my latest attempt.
#override
Widget build(BuildContext context) {
return SizedBox(
height: 900,
child: StreamBuilder(
stream: () async{
fireFirestore.instance.collection('properties').where('uid', isEqualTo: await _authService.getUserId()) .snapshots(),
},
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData)
return const Text('Loading...');
else {
return ListView.builder( ...............
in case you need to see the getUserId():
Future<String> getUserId() {
return _auth.currentUser().then((value) => value.uid.toString());
}
(i've done this method in both the future way (.then) and the async way (async await)
it just tells me the argument type Future<null> can't be assigned to the parameter type Stream
First, you're passing an async function as a stream, hence your error. Second, you need to wrap your StreamBuilder in a FutureBuilder since it depends on the future _authService.getUserId().
#override
Widget build(BuildContext context) {
return SizedBox(
height: 900,
child: FutureBuilder(
future: _authService.getUserId(),
builder: (context, snapshot) {
if(snapshot.hasData)
return StreamBuilder(
stream: fireFirestore.instance.collection('properties').where('uid', isEqualTo: snapshot.data) .snapshots(),
builder: (context, snapshot) {
...
},
);
return Text('future had no data');
},
),
);
}

Flutter Nested Stream builder -- last stream is getting null before showing data

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.

Using Stream Building with a specific Firestore document

I am building a Flutter application and I am having trouble understanding how to implement Firestore. Out of the tutorials I have seen, I only see how to create a snapshot of an entire collection, however in my case, my collection is users, so I only need to snapshot the document of a particular user. There doesn't appear to be documentation on the Firebase docs on how to do this nor is there much documentation on the FlutterFire GitHub page. Please help!
This is the Widget I'm trying to build with StreamBuilder.
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: Firestore.instance.collection('users').document(userId).snapshots(),
builder: (context, snapshot) {
return new ListView.builder(
itemCount: //what do I put here?,
itemBuilder: (context, index) => new Item(//And here?),
);
}
);
}
Lets say you want to create a Text with the name parameter from your document
Widget build(BuildContext context) {
String userId = "skdjfkasjdkfja";
return StreamBuilder(
stream: Firestore.instance.collection('users').document(userId).snapshots(),
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (!snapshot.hasData) {
return Text("Loading");
}
var userDocument = snapshot.data;
return Text(userDocument["name"]);
}
);
}
This is just one instance. Creating a StreamBuilder on the document will rebuild itself every time the document itself is changed. You can try this code, and then go to your console and change the "name" value. Your app will automatically reflect the changes.
Instead of just one Text, you could build entire tree that uses data from your stream.
If you want to get just at the moment value of the document, you can do so by resolving the Future of get() method on document reference.
var document = await Firestore.instance.collection('users').document(userId).get(),
Each element should be casted to have a reference later in the code.
return new StreamBuilder<DocumentSnapshot>(
stream: Firestore.instance.collection('users').document(userId).snapshots(), //returns a Stream<DocumentSnapshot>
builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;
return new Text(userDocument["name"]);
}
);
}
Update 2023 with null safety
class _UserInformationState extends State<UserInformation> {
final _usersStream = FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid) // 👈 Your document id change accordingly
.snapshots();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: _usersStream,
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
Map<String, dynamic> data =
snapshot.data!.data()! as Map<String, dynamic>;
return Text(data['fullName']);
},
),
),
);
}
}

Resources