StreamBuilder ListView returns empty list from Firestore on first load

Viewed 2295

In the app I'm building as part of the registration process a sub-collection, with up to 100 docs, is created for each 'User' document.

I'm trying to show these sub-collection documents in a StreamBuilder.

I have a curious bug that I can't resolve. The StreamBuilder doesn't display the data when the user first views it. Instead it returns an empty list.

I can see that the documents have been correctly generated within the sub-collection. The data is being set on a page before the page with the StreamBuilder. Even if there were latency I would have thought the new docs would have just started appearing within StreamBuilder. Firebase console view

The StreamBuilder does display the data as expected if the app is restarted - or if the user logs out and logs in again.

Below is the code I'm using:

Stream<QuerySnapshot> provideActivityStream() {
    return Firestore.instance
        .collection("users")
        .document(widget.userId)
        .collection('activities')
        .orderBy('startDate', descending: true)     
        .snapshots();
  }
...
Widget activityStream() {
  return Container(
      padding: const EdgeInsets.all(20.0),
      child: StreamBuilder<QuerySnapshot>(
      stream: provideActivityStream(),
      builder: (BuildContext context,
          AsyncSnapshot<QuerySnapshot> snapshot) {
        if (snapshot.hasError)
          return new Text('Error: ${snapshot.error}');
        if(snapshot.data == null) {
          return CircularProgressIndicator();
        }
        if(snapshot.data.documents.length < 1) {
          return new Text(
            snapshot.data.documents.toString()
            );
        }
        if (snapshot != null) {
          print('$currentUser.userId');
        }
        if (
          snapshot.hasData && snapshot.data.documents.length > 0
          ) {
          print("I have documents");
          return new ListView(
              children: snapshot.data.documents.map((
                  DocumentSnapshot document) {
                  return new PointCard(
                    title: document['title'],
                    type: document['type'],
                  );
                }).toList(),
            );
        }
      } 
    )
  );
}

Edit: Adding main build as per comment request

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Home"),
          actions: <Widget>[
          ],
          bottom: TabBar(
            tabs: [
              Text("Account"),
              Text("Activity"),
              Text("Links"),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            accountStream(),
            activityStream(),
            linksStream()
            ]
          )
        ),
      );
    }
  }

Attempts I've made to solve

I initially thought it was a connection error so created a series of cases based on switch (snapshot.connectionState). I can see that ConnectionState.active = true so thought adding a new document in Firestore might have an effect but does nothing.

I tried the following to make the initial stream constructor asynchronous. It fails to load any data.

Stream<QuerySnapshot> provideActivityStream() async* {
    await Firestore.instance
        .collection("users")
        .document(widget.userId)
        .collection('activities')
        .orderBy('startDate', descending: true)     
        .snapshots();
  }

I've tried removing the tabcontroller element - e.g. just having a single page - but that doesn't help either.

I've tried accessing the data using both a DocumentSnapshot and a QuerySnapshot. I have the problem with both.

I'm sure this is very straightforward but stuck on it. Any help greatly appreciated. Thanks!

3 Answers

It is not fetched by using any one of those query snapshot and document snapshot

You should query at first using Querysnapshot and then retrieve the information to a Documentsnapshot Yes, it may take a few seconds to load the document the solution is correct that you should async and await function

Instead of streamBuilder, I suggest you use Direct snapshot

we can load the document snapshot in the initstate of a statefullWidget works when your class is a statefullWidget and the problem is also related to the state

...

 bool isLoading;
 List<DocumentSnapshot> activity =[];
 QuerySnapshot user;
 @override
 void initState() {
  print("in init state");
  super.initState();
  getDocument();
 ``    } 
  getDocument() async{

 setState(() {
   isLoading = true;
 });
 user= await Firestore.instance
    .collection("users")
    .document(widget.userId)
    .collection('activities')
    .orderBy('startDate', descending: true)     
    .getDocuments();

  activity.isEmpty ? activity.addAll(user.documents) : null;

    setState(() {
  isLoading = false;
       });

     }

//inside  Widget build(BuildContext context) { return  Scaffold( in your body 
//section of scaffold in the cointainer
Container(
padding: const EdgeInsets.all(20.0),
child: isLoading ?
         CircularProgressIndicator(),
        :ListView.builder(

                          itemCount: global.category.length,
                          itemBuilder: (context, index) {
                            return  PointCard(
                                    title: activity[index].data['title'],
                                      type: activity[index].data['type'],
                                    );//pointcard
                             }
                            ),//builder
                     ),//container

We can also give a try for the following

 QuerySnapshot qs;
 Stream<QuerySnapshot> provideActivityStream() async{
    qs= await Firestore.instance
             .collection("users")
             .document(widget.userId)
             .collection('activities')
            .orderBy('startDate', descending: true)     
            .snapshots();

      return qs;
  }//this should work

but as per the basics of streambuilder if the above piece didn't work then there is another

     QuerySnapshot qs;
 Stream<QuerySnapshot> provideActivityStream() async* {
    qs= await Firestore.instance
             .collection("users")
             .document(widget.userId)
             .collection('activities')
            .orderBy('startDate', descending: true)     
            .snapshots();

      yield qs;
  }//give this a try

tl;dr

  • Needed to use setState to get the Firebase currentUser uid to be available for the widgets
  • Needed to use AutomaticKeepAliveClientMixin to work correctly with TabBar
  • I think using the Provider package may be a better way of persisting the user's state but didn't in solving this problem

Explanation

My code gets the currentUser uid with a Future. As per the the SO answer here that's a problem because the widgets will all be built before FirebaseAuth can give back the uid. I initially tried to use initState to get the uid but that has exactly the same synchronous problem. Calling setState from the function to call the FirebaseAuth.instance allowed the widget tree to update.

I'm placing this widget within a TabBar widget. My understanding is that everytime a tab is removed from view it's disposed of so rebuilt when returned. This was causing further state issues. API docs for the AutomaticKeepAlive mixin are here

Solution code

With added comments in the hope they're helpful for someone else's understanding (or someone can correct my misunderstanding)

activitylist.dart

class ActivityList extends StatefulWidget {
// Need a stateful widget to use initState and setState later
  @override
  _ActivityListState createState() => _ActivityListState();
}

class _ActivityListState extends State<ActivityList> 
  with AutomaticKeepAliveClientMixin<ActivityList>{
  // `with AutomaticKeepAliveClientMixin` added for TabBar state issues

  @override
  bool get wantKeepAlive => true;
  // above override required for mixin

  final databaseReference = Firestore.instance;

  @override
    initState() {
      this.getCurrentUser(); // call the void getCurrentUser function
      super.initState();
  }

  FirebaseUser currentUser;

  void getCurrentUser() async {
    currentUser = await FirebaseAuth.instance.currentUser();
    setState(() {
      currentUser.uid;
    });
    // calling setState allows widgets to access uid and access stream
  }

  Stream<QuerySnapshot> provideActivityStream() async* {
    yield* Firestore.instance
        .collection("users")
        .document(currentUser.uid)
        .collection('activities')
        .orderBy('startDate', descending: true)     
        .snapshots();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container(
                  padding: const EdgeInsets.all(20.0),
                  child: StreamBuilder<QuerySnapshot>(
                  stream: provideActivityStream(),
                  builder: (BuildContext context,
                  AsyncSnapshot<QuerySnapshot> snapshot) {
                    if(snapshot.hasError) return CircularProgressIndicator();
                    if(snapshot.data == null) return CircularProgressIndicator();
                    else if(snapshot.data !=null) {
                      return new ListView(
                          children: snapshot.data.documents.map((
                              DocumentSnapshot document) {
                            return new ActivityCard(
                              title: document['title'],
                              type: document['type'],
                              startDateLocal: document['startDateLocal'],
                            );
                          }).toList(),
                        );
                    }
                  },
                )
            );
  }
}

home.dart

...
@override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Home"),
          actions: <Widget>[
          ],
          bottom: TabBar(
            tabs: [
              Text("Account"),
              Text("Activity"),
              Text("Links"),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            accountStream(),
            ActivityList(), // now calling a stateful widget in an external file
            linksStream()
            ]
          )
        ),
      );
    }
  }
Related