How to cache Firebase data in Flutter?

2020-02-11 07:55发布

问题:

In my app I build a list of objects using data from Firebase. Inside a StreamBuilder, I check if the snapshot has data. If it doesen't, I am returning a simple Text widget with "Loading...". My problem is that if I go to another page in the app, and then come back, you can see for a split second that it says 'Loading...' in the middle of the screen, and it is a bit irritating. I am pretty sure it is downloading the data from Firebase, and building the widget every time I come back to that page. And if I don't do the check for data, it gives me a data that I am trying to access data from null.

Is there a way to cache the data that was already downloaded, and if there has been no change in the data from Firebase, then just use the cached data?

Heres a redacted version of my code:

class Schedule extends StatefulWidget implements AppPage {
  final Color color = Colors.green;
  @override
  _ScheduleState createState() => _ScheduleState();
}

class _ScheduleState extends State<Schedule> {
  List<Event> events;
  List<Event> dayEvents;
  int currentDay;
  Widget itemBuilder(BuildContext context, int index) {
    // Some Code
  }
  @override
  Widget build(BuildContext context) {
    return Center(
      child: StreamBuilder(
        stream: Firestore.instance.collection('events').snapshots(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Text("Loading...");
          }
          events = new List(snapshot.data.documents.length);
          for (int i = 0; i < snapshot.data.documents.length; i++) {
            DocumentSnapshot doc = snapshot.data.documents.elementAt(i);

            events[i] = Event(
              name: doc["name"],
              start: DateTime(
                doc["startTime"].year,
                doc["startTime"].month,
                doc["startTime"].day,
                doc["startTime"].hour,
                doc["startTime"].minute,
              ),
              end: DateTime(
                doc["endTime"].year,
                doc["endTime"].month,
                doc["endTime"].day,
                doc["endTime"].hour,
                doc["endTime"].minute,
              ),
              buildingDoc: doc["location"],
              type: doc["type"],
            );
          }
          events.sort((a, b) => a.start.compareTo(b.start));
          dayEvents = events.where((Event e) {
            return e.start.day == currentDay;
          }).toList();
          return ListView.builder(
            itemBuilder: itemBuilder,
            itemCount: dayEvents.length,
          );
        },
      ),
    );
  }
}

回答1:

To be sure whether the data is coming from Firestore's local cache or from the network, you can do this:

          for (int i = 0; i < snapshot.data.documents.length; i++) {
            DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
            print(doc.metadata.isFromCache ? "NOT FROM NETWORK" : "FROM NETWORK");

In the case you described you are probably going to still see the loading screen when its "NOT FROM NETWORK". This is because it does take some time to get it from the local cache. Soon you will be able to ask for the query's metadata for cases with empty results.

Like others suggested, you can cache the results and you won't see this. First you can try to cache it in the Widget using something like:

  QuerySnapshot cache; //**

  @override
  Widget build(BuildContext context) {
    return Center(
      child: StreamBuilder(
        initialData: cache, //**
        stream: Firestore.instance.collection('events').snapshots(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Text("Loading...");
          }
          cache = snapshot.data; //**

This will make your widget remember the data. However, if this does not solve your problem, you would have to save it not in this widget but somewhere else. One option is to use the Provider widget to store it in a variable that lives beyond the scope of this particular widget.

Probably not related, but it's also a good idea to move the Firestore.instance.collection('events').snapshots() to initState(), save the reference to the stream in a private field and use that it StreamBuilder. Otherwise, at every build() you may be creating a new stream. You should be ready for build() calls that happen many times per second, whatever the reason.