How can you nest StreamBuilders in Flutter?

2,226

Solution 1

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

Solution 2

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

For more info, check this post

Share:
2,226
Anthony Sette
Author by

Anthony Sette

Apparently, this user prefers to keep an air of mystery about them... sometimes. These are my opinions, get triggered... Top five languages are as follows (and probably changed between the time I am writing this and the time you are reading it): Python, JS, Dart, TypeScript, C++ PHP is the worst language ever invented. Nevermind Java might be worse. Vue.js is the best Web Framework. Flutter is the best cross-platform development framework. Now time for a stupid joke since I'm writing this at 3:00 am after a long coding session: Why does Python live on land? Because its above C level. Ok, Goodnight my doods. I just realized all of this amazing profile work will have to be rewritten when I graduate college.

Updated on December 13, 2022

Comments

  • Anthony Sette
    Anthony Sette over 1 year

    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.

  • Anthony Sette
    Anthony Sette almost 5 years
    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!
  • Anthony Sette
    Anthony Sette almost 5 years
    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()
  • Anthony Sette
    Anthony Sette almost 5 years
    I am also getting the following error The element type 'Set<StreamBuilder<List>>' can't be assigned to the list type 'Widget'.
  • spenster
    spenster almost 5 years
    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...
  • Anthony Sette
    Anthony Sette almost 5 years
    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.
  • Anthony Sette
    Anthony Sette almost 5 years
    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.
  • spenster
    spenster almost 5 years
    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.
  • Anthony Sette
    Anthony Sette almost 5 years
    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 zipped
  • Anthony Sette
    Anthony Sette over 4 years
    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.