简体   繁体   中英

How check the ConnectionState of Firestore snapshot stream with StreamProvider?

This example from the cloud_firestore documentation uses a StreamBuilder and the ConnectionState of an AsyncSnapshot to handle the stream in its different states. Is there a similar way to manage the ConnectionState when accessing the stream via a StreamProvider instead of a StreamBuilder ? What is the best way of avoiding it to return null in the short while until it actually has documents from Firestore?

Here the example from the cloud_firestore docs with the StreamBuilder:

    class BookList extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return StreamBuilder<QuerySnapshot>(
          stream: Firestore.instance.collection('books').snapshots(),
          builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
            if (snapshot.hasError)
              return new Text('Error: ${snapshot.error}');
            switch (snapshot.connectionState) {
              case ConnectionState.waiting: return new Text('Loading...');
              default:
                return new ListView(
                  children: snapshot.data.documents.map((DocumentSnapshot document) {
                    return new ListTile(
                      title: new Text(document['title']),
                      subtitle: new Text(document['author']),
                    );
                  }).toList(),
                );
            }
          },
        );
      }
    }

I have a rather basic stream:

    List<AuditMark> _auditMarksFromSnapshot(QuerySnapshot qs) {
      return qs.documents.map((DocumentSnapshot ds) {
        return AuditMark.fromSnapshot(ds);
      }).toList();
    }

    Stream<List<AuditMark>> get auditMarks {
      return Firestore.instance
          .collection('_auditMarks')
          .snapshots()
          .map(_auditMarksFromSnapshot);
    }

This is accessed via a StreamProvider (have omitted other providers here):

    void main() async {
      runApp(MultiProvider(
        providers: [
          StreamProvider<List<AuditMark>>(
              create: (_) => DatabaseService().auditMarks, ),
        ],
        child: MyApp(),
      ));
    }

I have tried somehow converting the QuerySnapshot to an AsyncSnapshot<QuerySnapshot> but probably got that wrong. Could of course give the StreamProvider some initialData like so - but this is cumbersome, error prone and probably expensive:

    initialData: <AuditMark>[
      AuditMark.fromSnapshot(await Firestore.instance
          .collection('_auditMarks')
          .orderBy('value')
          .getDocuments()
          .then((value) => value.documents.first))

...but I am hoping there is a smarter way of managing the connection state and avoiding it to return null before it can emit documents?

I have been dealing with this and didn't want to declare an initialData to bypass this issue.

What I did was creating a StreamBuilder as the child of StreamProvider . So that I could use the snapshot.connectionState property of StreamBuilder in the StreamProvider.

Here's the code:

    return StreamProvider<List<AuditMark>>.value(
        value: DatabaseService().auditMarks,
        child: StreamBuilder<List<AuditMark>>(
                stream: DatabaseService().auditMarks,
                builder: (context, snapshot) {
                    if (!snapshot.hasError) {
                        switch (snapshot.connectionState) {
                            case ConnectionState.none: // if no connection
                                return new Text(
                                    "Offline!",
                                    style: TextStyle(fontSize: 24, color: Colors.red),
                                    textAlign: TextAlign.center,
                                );
                            case ConnectionState.waiting
                                // while waiting the data, this is where you'll avoid NULL
                                return Center(child: CircularProgressIndicator());
                            default:
                                return ListView.builder(
                                    // in my case I was getting NULL for itemCount
                                    itemCount: logs.length,
                                    itemBuilder: (context, index) {
                                        return LogsTile(log: logs[index]);
                                    },
                                );
                        }
                    }
                    else {
                        return new Text(
                            "Error: ${snapshot.error}",
                            style: TextStyle(fontSize: 17, color: Colors.red),
                            textAlign: TextAlign.center,
                        );
                    }
                }
        )
    );

Probably not the most elegant solution, but I ended up using a simple bool variable which is true while not all StreamProviders have emitted values.

bool _waitForStreams = false;
if (Provider.of<List<AuditMark>>(context) == null) _waitForStreams = true;
if (Provider.of<...>>(context) == null) _waitForStreams = true; 
(etc. repeat for every StreamProvider)

// show "loading..." message while not all StreamProviders have supplied values
if (_waitForStreams) {
  return Scaffold(
    appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 25.0),
              Text('loading...'),
            ],
          )
        ],
      ),
    );
}

I don't know if it's correct but this is how I implement it.

Since streamProviser does not provide a connection state, I first use streamBuilder and then provider.value to distribute the data:

return StreamBuilder<BusinessM>(
    stream: db.businessDetails(), //firebase stream mapped to business model class
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.active)
        return Provider<BusinessM>.value(
          value: snapshot.data,
          child: Businesspage(),
        );
      else
        return Center(child: CircularProgressIndicator());
    });

For someone who want to use StreamProvider but end up with no ConnectionState state to use. For some of the cases, null represent the state of "waiting for the first data" , not "no data" .

In StreamProvider , there is no build-in method to detect the state. But we can still warp the state outside of the data:

StreamProvider<AsyncSnapshot<QuerySnapshot?>>.value(
  initialData: const AsyncSnapshot.waiting(),
  value: FirebaseFirestore.instance
      .collection('books')
      .snapshots()
      .map((snapshot) {
    return AsyncSnapshot.withData(ConnectionState.active, snapshot);
  }),
  child: ...,
);

or for firebaseAuth:

StreamProvider<AsyncSnapshot<User?>>.value(
  initialData: const AsyncSnapshot.waiting(),
  value: FirebaseAuth.instance.userChanges().map(
    (user) => AsyncSnapshot.withData(ConnectionState.active, user),
  ),
),

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM