How can you nest StreamBuilders in Flutter? - flutter

I have 2 Streams that I need to combine to build a widget, but unlike other questions I have seen I need to nest my streams.
I have a stream that gets a collection of documents from Firestore, and a stream that depends on data from the first to get a subcollection of documents. I would like to combine these into one stream, but they need to be nested since each document has its own subcollection of documents.
Stream 1 (Gets a collection of habits from FireStore):
Stream<List> getHabits(){
final Stream<QuerySnapshot> documents = Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.snapshots();
Stream<List> data = documents.map((doc) {
List data;
final documents = doc.documents;
///Maybe this would work to get history of each doc?
for(int i = 0; i < documents.length; i++){
///not sure what to do
getHistory(documents[i].documentID, DateTime.utc(2019,7,7), DateTime.now());
}
data = documents.map((documentSnapshot) => documentSnapshot).toList();
return data;
});
return data;
}
Stream 2 (Called in Stream 1, Takes DocumentID as a parameter, gets sub-collection of documents):
Stream<List> getHistory(String id, DateTime start, DateTime end) async* {
await for (QuerySnapshot querySnapshot in Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.document(id)
.collection("history")
.where('day', isGreaterThanOrEqualTo: start)
.where('day', isLessThanOrEqualTo: end)
.snapshots()) {
List history;
final documents = querySnapshot.documents;
history = documents.map((documentSnapshot) => documentSnapshot).toList();
yield history;
}
}
Any help on how I can combine these streams in a nested format into one stream to be used with StreamBuilder in flutter would be appreciated!'
EDIT
I am not sure if I am working in the right direction or not but I have tried to implement the solution from spenster and this is what I have at the moment in addition to the functions above.
StreamBuilder<List>(
stream: getHabits(),
initialData: [],
builder: (context, snapshot) {
List<UserHabit> habits = [];
List<Widget> test = List.generate(snapshot.data.length, (index){
List<History> history = [];
DocumentSnapshot doc = snapshot.data[index];
return StreamBuilder(
stream: getHistory(doc.documentID, DateTime.utc(2019,7,7), DateTime.now()),
builder: (context, snapshot) {
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Text('Loading...');
default:
if(!snapshot.data.isEmpty){ //history collection exists
for(int i = 0; i < snapshot.data.length; i++){
//add to history
history.add(History(
day: snapshot.data[i]['day'].toDate(),
dateCompleted: snapshot.data[i]['dateCompleted'].toDate(),
morning: snapshot.data[i]['morning'],
afternoon: snapshot.data[i]['afternoon'],
evening: snapshot.data[i]['evening'],
anytime: snapshot.data[i]['anytime'],
));
}
}
habits.add(UserHabit(
name: doc['habit'],
color: doc['color'],
icon: doc['icon'],
repeat: doc['repeat'],
daily: doc['daily'],
weekly: doc['weekly'],
monthly: doc['monthly'],
time: doc['time'],
history: history,
));
print(habits); //returns each iteration of assembling the list
return Text("i dont want to return anything");
}
},
);
}
);
print(habits); //returns empty list before anything is added
return Column(
children: test,
);
},
),
The Class for UserHabits and History can be shared, but they are just basic classes that assign types and allow easy access.

I have done something similar simply using nested StreamBuilders. Depending on how you want your Widgets organized, you can create streams within the outer StreamBuilder. Based on your clarifying comments, this is one possibility:
#override
Widget build(BuildContext context) {
var habits = Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: habits,
builder: (context, snapshot) {
if (!snapshot.hasData)
return Text("Loading habits...");
return ListView(children: snapshot.data.documents.map((document) {
var query = Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.document(document.documentID)
.collection("history")
.where('day', isGreaterThanOrEqualTo: start)
.where('day', isLessThanOrEqualTo: end)
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: query,
builder: (context, snapshot) {
if (!snapshot.hasData) return Text("Loading...");
// right here is where you need to put the widget that you
// want to create for the history entries in snapshot.data...
return Container();
},
);
}).toList());
},
);
}

Try merging your streams with something like Observable.zip2(stream1,stream2,zipper) or Observable.combineLatest2(streamA, streamB, combiner).
For more info, check this post

Related

how to perform query to Firestore and order result Randomly in Flutter

I am retrieving specific documents from firestore collection using flutter stream builder.
the issue is I would like to display the results every single time in a different order (Randomely).
the stream is the below:
stream: FirebaseFirestore.instance
.collection('BusinessProfilesCollection')
.where('Profile_direct_category',
isEqualTo: selecteddirectcategory)
.where('Profile_status', isEqualTo: "Active")
.where('Profile_visibility', isEqualTo: "Yes")
.where('Profile_city',
isEqualTo: globaluserdefaultcity)
.where('Profile_pinning_status',
isEqualTo: "No")
.snapshots(),
the problem is everytime the user do the query the data is returned in the same order. I would like to shuffle it somehow so I remove any advantage from any profile. (document)
I assume you have a list somewhere, where you display your documents? If so, you can use the .shuffle() operator on it! Example:
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
String selecteddirectcategory = 'selecteddirectcategory';
String globaluserdefaultcity = 'globaluserdefaultcity';
class RandomResultsScreen extends StatefulWidget {
#override
_RandomResultsScreenState createState() {
return _RandomResultsScreenState();
}
}
class _RandomResultsScreenState extends State<RandomResultsScreen> {
Stream<QuerySnapshot> myStream = FirebaseFirestore.instance
.collection('BusinessProfilesCollection')
.where('Profile_direct_category', isEqualTo: selecteddirectcategory)
.where('Profile_status', isEqualTo: "Active")
.where('Profile_visibility', isEqualTo: "Yes")
.where('Profile_city', isEqualTo: globaluserdefaultcity)
.where('Profile_pinning_status', isEqualTo: "No")
.snapshots();
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<QuerySnapshot>(
stream: myStream,
builder: (context, asyncSnapshot) {
List<Widget> docs = [];
QuerySnapshot? foundResults = asyncSnapshot.data;
if (foundResults == null) {
//It always wants to be null at first, and then you get errors for calling on null.
return Center(child: CircularProgressIndicator());
} else {
for (QueryDocumentSnapshot doc in foundResults.docs) {
Map<String, dynamic> docData = doc.data() as Map<String, dynamic>;
docs.add(
MyWidget(docData) // Some Widget that you use to display your data
);
}
docs.shuffle(); // <- Where the magic randomization happens!
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) {
return docs[index];
},
);
}
},
),
);
}
}

Firestore how to fetch specific data with specific user id in Flutter

I have stream builder, and I fetch all the users. After that, using bloc (or any state management) I filter them. After filtering, I create a Set which has filtered user ids (I mean there is a set, and it has user ids).
Now, using with these uids I want to fetch filtered user datas. I did with FirebaseFirestore.instance.collection(...).doc(userId).get(), after that it gives Future<String?>. What should I do?
here is the codes:
class HomePageBody extends StatelessWidget {
HomePageBody({
Key? key,
required this.mapsState,
}) : super(key: key);
final MapsState mapsState;
final Set users = {};
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: firestoreStream,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.none) {
return const CustomProgressIndicator(
progressIndicatorColor: blackColor,
);
} else if (!snapshot.hasData) {
return const CustomProgressIndicator(
progressIndicatorColor: blackColor,
);
} else if (snapshot.hasData) {
final usersDatas = snapshot.data.docs;
for (var userDatas in usersDatas) {
if (userDatas["latitude"] == null || userDatas["longitude"] == null) {
} else {
users.add(userDatas);
}
}
context.read<MapsCubit>().filterUsersWithRespectToDistance(users: users);
final usersWithInTenKilometers = mapsState.usersWithInTenKilometers;
**// HERE WE HAVE FILTERED USERS, AND THIS SET HAS USER IDS.**
return ListView.builder(
padding: const EdgeInsets.only(top: 75),
itemCount: usersWithInTenKilometers.length,
itemBuilder: (context, index) {
final userId = usersWithInTenKilometers.elementAt(index);
final usersDatas = FirebaseFirestore.instance
.collection("users")
.doc(userId)
.get();
// I did like this, but it does not work.
return CustomListTile(
userImageUrl: "https://picsum.photos/200/300",
userStatus: "userStatus",
userName: "userName",
);
},
);
}
return const CustomProgressIndicator(
progressIndicatorColor: blackColor,
);
},
);
}
}
Consequently, I have a Set (or you can think like List), and it has user ids. Using these user ids, fetch user datas basically from the Firestore (email: ..., password: ... etc)
final userId = usersWithInTenKilometers.elementAt(index);
final users = FirebaseFirestore.instance
.collection("users")
.doc(userId)
.get()
.then((value) => value)
.then((value) => value.data());
return FutureBuilder(
future: users,
builder: (context, snapshot) {
if (snapshot.hasData) {
final convertUserDataToMap =
Map<String, dynamic>.from(snapshot.data as Map<dynamic, dynamic>);
final List userDataList = convertUserDataToMap.values.toList();
final userId = userDataList[0];
final userLong = userDataList[1];
....
I solved like this
Since you get back a Future<String?>, I'd typically first consider using a FutureBuilder to render that value.
If you have multiple values that each is loaded asynchronously separately (like is the case here with your multiple get() calls), I'd start with using a separate FutureBuilder for each Future. Only if I'd run into practical problems with that, would I start considering more complex options, such as Future.wait() to wait for all of them to complete before rendering any result.

Combine two stream-queries in flutter

I want to create a streambuilder to download multiple user-profiles from firebase. But to know which users are needed I have to get the user-ids at first, these are stored in an array. So I created a method to download the array and after this is done the streambuilder loads the user-data for each user-id from the array. Here's the code:
Method to get the array (executed in initState()):
Stream<QuerySnapshot> stream() async* {
job = Job.fromJson(await FirebaseFirestore.instance
.collection("jobs")
.doc(widget.jobId)
.get());
applicants = job.applicants;
await FirebaseFirestore.instance
.collection('users')
.where('uid', whereIn: applicants)
.snapshots();
}
And the streambuilder in the scaffolds' body:
body: isLoading
? Center(child: Container(child: CircularProgressIndicator()))
: applicants.isEmpty
? Center(
child: Text("no values"),
)
: StreamBuilder<QuerySnapshot>(
stream: stream(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else { xy }
So my question is if there's a possibility to combine the first method and the stream. Because at the moment the user can't get any update if an application is withdrawn while using the screen.

Flutter - How do I use await inside the streambuilder?

I want to use await inside streambuilder. However, if you use async inside, you get an error. On the code below !!!!!!!! That's the part I want to solve. Thank you very much if I can tell you how.
class _MemoStreamState extends State<MemoStream> {
final _fireStore = Firestore.instance;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _fireStore
.collection(widget.logInUsrEmail)
.orderBy('id', descending: false)
.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
final memos = snapshot.data.documents;
List<MemoMaterial> memoList = [];
for (var memo in memos) {
final memoDocumentID = memo.documentID;
final memoTitle = await PlatformStringCryptor().decrypt(memo.data['title'], _key); !!!!!!!!!!
final memoUsrID = memo.data['usrID'];
final memoUsrPW = memo.data['usrPW'];
final memoText = memo.data['text'];
final memoCreateTime = memo.data['createTime'];
final memoMaterial = MemoMaterial(
logInUsrEmail: widget.logInUsrEmail,
doc: memoDocumentID,
title: memoTitle,
usrID: memoUsrID,
usrPW: memoUsrPW,
text: memoText,
createTime: memoCreateTime,
);
memoList.add(memoMaterial);
}
return Expanded(
child: new ListView.builder(
You should do something like this :
Stream<List<MemoMaterial>> memosStream;
Future<MemoMaterial> generateMemoMaterial(Memo memo) async {
final memoTitle =
await PlatformStringCryptor().decrypt(memo.data['title'], _key);
return MemoMaterial(
logInUsrEmail: widget.logInUsrEmail,
doc: memo.documentID,
title: memoTitle,
usrID: memo.data['usrID'],
usrPW: memo.data['usrPW'],
text: memo.data['text'];,
createTime: memo.data['createTime'],
);
}
#override
void initState() {
memosStream = _fireStore
.collection(widget.logInUsrEmail)
.orderBy('id', descending: false)
.snapshots()
.asyncMap((memos) => Future.wait([for (var memo in memos) generateMemoMaterial(memo)]));
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<List<MemoMaterial>>(
stream: memosStream // Use memostream here
asyncMap() will "transform" every new set of Documents into a list of MemoMaterial, and emit this list into the stream when the action is performed.
Future.wait() allows to perform multiple async requests simultaneously.
You can do it using FutureBuilder inside StreamBuilder in following way.
Stream<List<int>> callme() async* {
yield [1, 2, 3, 4, 5, 6];
}
buildwidget() async {
await Future.delayed(Duration(seconds: 1));
return 1;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: StreamBuilder(
stream: callme(),
builder: (_, sna) {
if (sna.hasData) {
return FutureBuilder(
future: buildwidget(),
builder: (_, snap) {
if (snap.hasData) {
return ListView.builder(
itemCount: sna.data.length,
itemBuilder: (_, index) {
return Text("${sna.data[index]} and ${snap.data}");
},
);
} else {
return CircularProgressIndicator();
}
},
);
} else {
return CircularProgressIndicator();
}
}),
),
);
}
I will prefer to use Getx or Provider State management to Handle the UI if it depends on the async function.
Suppose you want to fetch data from firebase using StreamBuilder() which returns some docs which contains image links then you want to download these images and show from storage. Obviously downloading the image is async type of work. Then you will get error if you show the images with the links you get direct from StreamBuilder().
What you can do is set a variable in getx or provider to show or hide the image Widget. If the Image is being downloaded or not downloaded then set the variable to hide/show the image when the async type of function is completed.

Filter Stream to List

I have the following stream builder:
streamCard() {
return StreamBuilder(
stream: cardsRef
.orderBy("timestamp", descending: true)
.limit(10)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
List<CustomCard> cards = [];
snapshot.data.documents.forEach((doc) {
cards.add(CustomCard.fromDocument(doc));
});
...
return Stack(
alignment: Alignment.center,
children: cards,
);
I need to filter certain cards from being added to the stream/displayed when:
I'm the owner of the card ('ownerId' == currentUserId)
I've liked the card ('liked.' contains = currentUserId)
OwnerId is a field inside each document and Liked is an array with Id's who have liked it.
I've tried to remove the cards from being added to the cards List<> with .where and .contains, but couldn't properly 'discard' them. I was thinking another option could be to modify the Stack directly, in
children: cards
with cards.removeWhere/.skip, or something like that.
To follow the bloc pattern, business logic should happen in your bloc class. Which mean that you should do all the sorting or filtering in the bloc. When you add a new object into sink, the streambuilder will rebuild.
class BlaBloc {
final BehaviorSubject<List<String>> _results =
BehaviorSubject<List<String>>();
getResults() {
List<String> yourresults = yourapi.getResults();
_results.sink.add(yourresults);
};
getResultsLikedByMe() {
List<String> yourresults = yourapi.getResultsLikedByMe();
_results.sink.add(yourresults);
}
getResultsOwnerIsMe() {
List<String> yourresults = yourapi.getResultsOwnerIsMe();
_results.sink.add(yourresults);
}
BehaviorSubject<List<String>> get results => _results;
}
final blaBloc = BlaBloc();
When you build your streambuilder, point to your Bloc, for example as below:
body: StreamBuilder<List<String>>(
stream: blaBloc.results,
builder: (context, AsyncSnapshot<RecipesResponse> snapshot) {
// build your list here...
}
)
To understand more about Bloc pattern, this is a very useful tutorial you could follow here: RxDart Bloc Pattern