Anthony Sette
Anthony Sette

Reputation: 837

How can you nest StreamBuilders in 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;
    }
}

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.

Upvotes: 3

Views: 3352

Answers (2)

Sergio Bernal
Sergio Bernal

Reputation: 2327

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

For more info, check this post

Upvotes: 1

spenster
spenster

Reputation: 538

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

Upvotes: 5

Related Questions