如何在 Flutter 中缓存 Firebase 数据?

Posted

技术标签:

【中文标题】如何在 Flutter 中缓存 Firebase 数据?【英文标题】:How to cache Firebase data in Flutter? 【发布时间】:2019-12-14 14:17:03 【问题描述】:

在我的应用中,我使用 Firebase 中的数据构建了一个对象列表。在 StreamBuilder 中,我检查快照是否有数据。如果没有,我将返回一个带有“正在加载...”的简单文本小部件。我的问题是,如果我转到应用程序中的另一个页面,然后再返回,您会在一瞬间看到它在屏幕中间显示“正在加载...”,这有点烦人。我很确定它正在从 Firebase 下载数据,并在我每次返回该页面时构建小部件。如果我不检查数据,它会给我一个我试图从 null 访问数据的数据。

有没有办法缓存已经下载的数据,如果Firebase的数据没有变化,那么就使用缓存的数据?

这是我的代码的修订版本:

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

【问题讨论】:

你在什么平台上运行这个应用程序?您使用什么插件来访问 Firestore?在 iosandroid 上,FlutterFire plugin 应该已经在您访问数据后在本地缓存它。当您第一次打开集合上的流时,应将数据添加到缓存中。随后对同一集合的stream() 调用应该“立即”从本地缓存返回数据,然后可能稍后再次触发来自服务器的更新。 来自本地缓存的数据(如果启用)不会立即出现在您的应用中。如果可以的话,您必须等待本地缓存满足查询。如果您想要即时结果,请将内容存储在内存中,并且不要使用 SDK 执行其他查询。 【参考方案1】:

要确定数据是来自 Firestore 的本地缓存还是来自网络,您可以这样做:

          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");

在您描述的情况下,您可能仍会看到“不是来自网络”的加载屏幕。这是因为从本地缓存中获取它确实需要一些时间。很快您就可以向query's metadata 询问结果为空的案例。

像其他人建议的那样,您可以缓存结果而您不会看到这一点。首先,您可以尝试使用以下方式将其缓存在 Widget 中:

  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; //**

这将使您的小部件记住数据。但是,如果这不能解决您的问题,您将不得不将其保存在此小部件中,而不是保存在其他地方。一种选择是使用Provider 小部件将其存储在超出此特定小部件范围的变量中。

可能不相关,但将Firestore.instance.collection('events').snapshots() 移动到initState() 也是一个好主意,将流的引用保存在私有字段中并使用它StreamBuilder。否则,在每次 build() 时,您可能会创建一个新流。无论出于何种原因,您都应该为每秒发生多次的build() 调用做好准备。

【讨论】:

“像其他人建议的那样,你可以缓存结果,你不会看到这个。”我该怎么做呢? @Marco 正在为 firebase 寻找类似的东西?pub.dev/packages/flutter_cache_store【参考方案2】:

您可以使用以下代码来定义要从中检索数据的源。这将在本地缓存或服务器上搜索,而不是同时搜索。它适用于所有get() 参数,无论是搜索还是文档检索。

import 'package:cloud_firestore/cloud_firestore.dart';

FirebaseFirestore.instance.collection("collection").doc("doc").get(GetOptions(source: Source.cache))

要检查搜索是否在缓存中有数据,您需要首先对缓存运行搜索,如果没有结果,则对服务器运行它。 我发现项目firestore_collection 使用了一个可以大大简化此过程的简洁扩展。

import 'package:cloud_firestore/cloud_firestore.dart';

// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_document.dart
extension FirestoreDocumentExtension on DocumentReference 
  Future<DocumentSnapshot> getSavy() async 
    try 
      DocumentSnapshot ds = await this.get(GetOptions(source: Source.cache));
      if (ds == null) return this.get(GetOptions(source: Source.server));
      return ds;
     catch (_) 
      return this.get(GetOptions(source: Source.server));
    
  


// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_query.dart
extension FirestoreQueryExtension on Query 
  Future<QuerySnapshot> getSavy() async 
    try 
      QuerySnapshot qs = await this.get(GetOptions(source: Source.cache));
      if (qs.docs.isEmpty) return this.get(GetOptions(source: Source.server));
      return qs;
     catch (_) 
      return this.get(GetOptions(source: Source.server));
    
  

如果您添加此代码,您只需将文档和查询的.get() 命令更改为.getSavy(),它将自动先尝试缓存,如果本地找不到数据,则仅联系服务器。

FirebaseFirestore.instance.collection("collection").doc("doc").getSavy();

【讨论】:

以上是关于如何在 Flutter 中缓存 Firebase 数据?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Flutter Firebase 中清除以前的用户缓存数据?

Firebase 托管 Flutter Web 应用程序未清除首次部署的缓存

在flutter中缓存两个List

如何在 Firebase 分析中跟踪 Flutter 屏幕?

如何在项目中使用 firebase 和 firebase 功能的现有 Flutter 应用程序中更新包名称?

如何在 Flutter 中等待文件上传到 Firebase 存储?