Firestore 与 Firebase 的离线问题

Posted

技术标签:

【中文标题】Firestore 与 Firebase 的离线问题【英文标题】:Offline issue with Firestore vs Firebase 【发布时间】:2018-05-30 07:20:20 【问题描述】:

我将我的一个应用转换为新的 Firestore。我正在做一些事情,例如在单击按钮时保存文档,然后在 onSuccess 侦听器中转到不同的活动。

我还利用 Firestore 保存操作返回任务这一事实,使用 Tasks.whenAll 将任务组合在一起:

val allTasks = Tasks.whenAll(
       createSupporter(supporter),,
       setStreetLookup(makeStreetKey(supporter.street_name)),
       updateCircleChartForUser(statusChange, createMode = true), 
       updateStatusCountForUser(statusChange))

      allTasks.addOnSuccessListener(this@SignUpActivity, successListener)
      allTasks.addOnFailureListener(this@SignUpActivity, onFailureListener)

最后,我从成功保存中获取文档 ID,并将其存储在首选项或本地数据库中以供以后使用(在 onSuccessListener 内)

这一切都很好。 直到网络连接中断。然后一切都崩溃了,因为任务永远不会完成,并且 onSuccess/onFailure/onComplete 侦听器永远不会被调用。所以应用程序只是挂起。

我正在通过在每次保存之前检查网络可用性来解决此问题,然后通过创建没有任何侦听器的任务来解决此问题。我还在使用 UUID 生成器在本地生成文档 ID。

顺便说一句,这不是该应用与旧版 Firebase 一起工作的方式。在这种情况下,离线时一切都运行良好,而且每当应用程序上线时,我看到文档会同步。

我对 Firestore 的解决方法似乎很糟糕。有没有人提出更好的解决方案?

查看相关Firestore database on insert/delete document callbacks not being invoked when there is no connection addOnCompleteListener not called offline with cloud firestore

【问题讨论】:

你试过超时吗?有几种实现方式,只需在经过一段时间(比如 3-5 秒)后执行取消操作。 Task api 不会直接让您取消任务。所以你必须以其他方式做到这一点。 需要注意的一点是,当您使用 Firestore 进行添加/更新时,它会立即在本地缓存中创建一条记录。然后它会在可能的情况下尝试与服务器同步。所以它几乎就像一个两阶段提交。但是一旦数据在本地缓存中,它似乎不会被回滚。侦听器正在等待服务器同步,但如果有办法让侦听器返回本地同步,那就更好了。 正如亚历克斯在他的回答中所说的“当网络连接丢失(用户设备上没有网络连接)时,不会触发 onSuccess() 和 onFailure()。”。这也一直是 Firebase 实时数据库中的行为:只有在服务器上提交的写入才被视为失败/成功,在此之前它们处于挂起状态。 @Frank Firebase 根本没有 onSuccess()/onFailure() 监听器。它是完全异步的,所以当应用程序启动/离线时可能会发生一些事情,我不必知道它。它只是工作。 Firestore 也声称是异步的,但实际上文档并不清楚这些差异。 【参考方案1】:

Cloud Firestore 为我们提供了处理离线数据的功能,但您需要使用“快照”(QuerySnapshot、DocumentSnapshot)来处理这种情况,遗憾的是它没有很好地记录。这是一些使用快照处理案例的代码示例(我使用 Kotlin android):

更新数据:

db.collection("members").document(id)
  .addSnapshotListener(object : EventListener<DocumentSnapshot> 
      override fun onEvent(snapshot: DocumentSnapshot?,
                           e: FirebaseFirestoreException?) 
          if (e != null) 
              Log.w(ContentValues.TAG, "Listen error", e)
              err_msg.text = e.message
              err_msg.visibility = View.VISIBLE;
              return
          
          snapshot?.reference?.update(data)

      
  )

添加数据:

db.collection("members").document()
 .addSnapshotListener(object : EventListener<DocumentSnapshot> 
     override fun onEvent(snapshot: DocumentSnapshot?,
                          e: FirebaseFirestoreException?) 
         if (e != null) 
             Log.w(ContentValues.TAG, "Listen error", e)
             err_msg.text = e.message
             err_msg.visibility = View.VISIBLE;
             return
         
         snapshot?.reference?.set(data)

     
 )

删除数据:

db.collection("members").document(list_member[position].id)
   .addSnapshotListener(object : EventListener<DocumentSnapshot> 
       override fun onEvent(snapshot: DocumentSnapshot?,
                            e: FirebaseFirestoreException?) 
           if (e != null) 
               Log.w(ContentValues.TAG, "Listen error", e)
               return
           
           snapshot?.reference?.delete()
       
   )

您可以在此处查看代码示例:https://github.com/sabithuraira/KotlinFirestore 和博客文章http://blog.farifam.com/2017/11/28/android-kotlin-management-offline-firestore-data-automatically-sync-it/

【讨论】:

实际上这个方法只是基本上忽略了 update() 调用返回的未来。如果你在 update() 调用上安装了一个监听器(即使在快照监听器中),你仍然会看到它没有被调用。因此,从功能上讲,这与直接忽略未来完全相同。而且这种方法存在更新会被多次调用的问题。【参考方案2】:

当网络连接丢失时(用户设备上没有网络连接),onSuccess()onFailure() 都不会被触发。这种行为是有道理的,因为只有在 Firebase 服务器上提交(或拒绝)数据时才认为任务完成。所以onSuccess()只会在任务成功完成时触发。

无需在每次保存前检查网络可用性。有一个解决方法可以轻松帮助您查看 Firestore 客户端是否确实无法连接到 Firebase 服务器,即enabling debug logging

FirebaseFirestore.setLoggingEnabled(true);

将数据写入 Firestore 数据库的操作在实际提交到后端后被定义为 signal completion。因此,这是按预期工作的:离线时它们不会发出完成信号。

请注意,Firestore 客户端在内部保证您可以读取自己的写入内容,即使您不等待删除任务完成也是如此。 Firestore 客户端旨在在没有互联网连接的情况下继续正常运行。所以在没有互联网连接的情况下写入/删除数据库是可能的(按设计),并且永远不会产生错误。

【讨论】:

我不明白打开日志记录是一种“解决方法”。基本问题是如何使 Firestore 应用程序离线和在线工作相同。显然,我不希望用户整天等待听众在他们的设备掉线时触发。 在线时不能像离线时一样执行 Firestore 工作。这就是为什么我们有两种不同的行为,因为有两种不同的状态。当设备离线时,当本地缓存的数据发生变化时,你的监听器会收到监听事件。您可以收听文档、集合和查询,但这些方法只有在您重新联机并且只有在数据被服务器提交或拒绝时才会被调用。 此外,如果您想检查您是从服务器接收数据还是从缓存接收数据,请在快照事件中使用 SnapshotMetadata 上的 fromCache 属性。如果 fromCache 是true,则数据来自缓存并且可能不完整。如果fromCachefalse,则数据是完整的并且是最新的,并且在服务器上具有最新更新。【参考方案3】:

我通过http://blog.farifam.com 的信息了解了如何操作。 基本上您必须使用SnapshotListeners 而不是OnSuccess 侦听器进行离线工作。 此外,您不能使用 Google 的任务,因为它们不会离线竞争。

相反(因为任务基本上是承诺),我使用了 Kotlin Kovenant 库,它可以将侦听器附加到承诺。一个问题是您必须将 Kovenant 配置为允许对一个 Promise 进行多次解析,因为事件监听器可以被调用两次(一次是在将数据添加到本地缓存时,一次是在同步到服务器时)。

这是一个示例 sn-p 代码,带有成功/失败侦听器,可在线和离线运行。

val deferred = deferred<DocumentSnapshot, Exception>() // create a deferred, which holds a promise
// add listeners
deferred.promise.success  Log.v(TAG, "Success! docid=" + it.id) 
deferred.promise.fail  Log.v(TAG, "Sorry, no workie.") 

val executor: Executor = Executors.newSingleThreadExecutor()
val docRef = FF.getInstance().collection("mydata").document("12345")
val data = mapOf("mykey" to "some string")

docRef.addSnapshotListener(executor, EventListener<DocumentSnapshot>  snap: DocumentSnapshot?, e: FirebaseFirestoreException? ->
    val result = if (e == null) Result.of(snap) else Result.error(e)
    result.failure 
        deferred.reject(it) // reject promise, will fire listener
    
    result.success  snapshot ->
        snapshot.reference.set(data)
        deferred.resolve(snapshot) // resolve promise, will fire listener
    
)

【讨论】:

【参考方案4】:

对于离线支持,您需要设置Source.CACHE

docRef.get(Source.CACHE).addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() 
    @Override
    public void onComplete(@NonNull Task<DocumentSnapshot> task) 
        if (task.isSuccessful()) 
            // Document found in the offline cache
            DocumentSnapshot document = task.getResult();

         else 
            //error
        
    
);

【讨论】:

【参考方案5】:

自从我在 Flutter 中使用以来,此代码为 Dart 语言,但您可以轻松地将其更改为您的平台和语言

Future<void> updateDoc(String docPath, Map<String, dynamic> doc) async 
    doc["updatedAt"] = Utils().getCurrentTimestamp();
    DocumentReference documentReference = _firestore.doc(docPath);

    Completer completer = Completer();
    StreamSubscription streamSubscription;
    streamSubscription = documentReference
        .snapshots(includeMetadataChanges: true)
        .listen((DocumentSnapshot updatedDoc) 
      // Since includeMetadataChanges is true this will stream new data as soon as
      // it update in local cache so it data has same updateAt it means it new data
      if (updatedDoc.data()["updatedAt"] == doc["updatedAt"]) 
        completer.complete();
        streamSubscription.cancel();
      
    );
    documentReference.update(doc);
    return completer.future;
  

因此,由于设置了includeMetadataChanges,它会在本地缓存更改时将数据发送到流,因此当您调用更新时,您将在本地缓存更新后立即收到数据。您可以使用Completer 来完成您的未来。现在您的方法只等待更新本地缓存,您可以将await 用于updateDoc

【讨论】:

以上是关于Firestore 与 Firebase 的离线问题的主要内容,如果未能解决你的问题,请参考以下文章

在 Flutter 中,如何使用 CachedNetworkImage 显示来自 Firebase 的离线图像?

firebase firestore 离线持久化 FirebaseError

使用 Firebase Storage 和 Firebase Firestore 创建离线第一个应用程序的最佳方法是啥? #AskFirebase

使用 Flutter 和 Firebase 实现实时在线/离线状态

Ionic + Phonegap - 添加授权的离线数据库

如何离线存储 Firebase 分析数据超过 72 小时?