How can you nest StreamBuilders in Flutter?
Asked Answered
C

2

3

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

How can I combine these streams in a nested format into one stream to be used with StreamBuilder in Flutter?

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.

Cranston answered 1/8, 2019 at 14:38 Comment(0)
S
5

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());
    },
  );
}
Sandoval answered 1/8, 2019 at 15:48 Comment(8)
Thank you! I'll give it a shot and if it works I will mark your answer as correct! I appreciate the time you took to answer it!Cranston
Just out of curiosity is there any way I can just not return anything in getHistory StreamBuilder? I need to use a list of history data to see if things were completed or not, but I am only building one widget per item in getHabits()Cranston
I am also getting the following error The element type 'Set<StreamBuilder<List>>' can't be assigned to the list type 'Widget'.Cranston
If you only want one document from your history colection, then you can use Firestore to select it (say using .limit(1) on your query), then build just a single widget. That should also incidentally eliminate the other error you're seeing too. I'll see if I can update the answer...Sandoval
Here I am going to update the question so you can see what I mean. I made some progress but I cannot access a variable that I modified in the second streambuilder outside of it. There is one stream that gets the habits, then the other gets the history for each habit. For the application I am working on I need to check to see say how many times something was completed in the last week before showing a widget. There is only one widget being returned per habit, but the widget requires all the history to assemble it if that makes sense.Cranston
If you have a chance please check my update so you can better understand my intentions. If I could use habits at the second print location I think this solution would work but I can't because StreamBuilder is asynchronous. Which is why I wanted to nest the streams before using StreamBuilder but I wasn't sure if that was possible.Cranston
Please see my update. All you have left to do there is create your widget based on the various history entries retrieved in the second StreamBuilder.Sandoval
I think I managed to get this method to work but I feel like there should be a way to nest streams without needed to nest streambuilders. Or firebase should make it easier to access sub-documents haha. Thank you so much for your time, I will mark this correct! Been working on this one dilemma for about a week.Cranston
H
1

Try merging your streams with something like Observable.zip2(stream1,stream2,zipper) or Observable.combineLatest2(streamA, streamB, combiner).

For more info, check this post

Herdsman answered 2/8, 2019 at 20:5 Comment(1)
I like where this is headed, but the second stream depends on the documentID of an item from the first stream. And the second stream is different for every item from the first. I need them to be nested in some manner rather than zippedCranston

© 2022 - 2024 — McMap. All rights reserved.