Firebase 离线功能和 addListenerForSingleValueEvent

Posted

技术标签:

【中文标题】Firebase 离线功能和 addListenerForSingleValueEvent【英文标题】:Firebase Offline Capabilities and addListenerForSingleValueEvent 【发布时间】:2015-12-28 00:44:11 【问题描述】:

每当我将addListenerForSingleValueEventsetPersistenceEnabled(true) 一起使用时,我只能设法从服务器获取DataSnapshot 的本地脱机副本,而没有更新后的DataSnapshot

但是,如果我将addValueEventListenersetPersistenceEnabled(true) 一起使用,我可以从服务器获取DataSnapshot 的最新副本。

这对于addListenerForSingleValueEvent 是否正常,因为它只在本地搜索DataSnapshot(离线)并在成功检索DataSnapshot ONCE(离线或在线)后删除其侦听器?

【问题讨论】:

【参考方案1】:

更新(2021 年)一个新方法调用(get on androidgetData on ios)实现了您想要的行为:它首先尝试从服务器获取最新值,只有在无法到达服务器时才回退到缓存中。使用持久侦听器的建议仍然适用,但至少有一个更简洁的选择,即使您启用了本地缓存,也可以一次性获取数据。


持久性的工作原理

Firebase 客户端会在内存中保留一份您正在积极收听的所有数据的副本。一旦最后一个监听器断开连接,数据就会从内存中刷新。

如果您在 Firebase Android 应用程序中启用磁盘持久性:

Firebase.getDefaultConfig().setPersistenceEnabled(true); 

Firebase 客户端将保存应用最近收听的所有数据的本地副本(在磁盘上)。

附加监听器时会发生什么

假设你有以下ValueEventListener

ValueEventListener listener = new ValueEventListener() 
    @Override
    public void onDataChange(DataSnapshot snapshot) 
        System.out.println(snapshot.getValue());
    

    @Override
    public void onCancelled(FirebaseError firebaseError) 
        // No-op
    
;

当您将ValueEventListener 添加到位置时:

ref.addValueEventListener(listener); 
// OR
ref.addListenerForSingleValueEvent(listener); 

如果位置的值在本地磁盘缓存中,Firebase 客户端将立即从本地缓存中调用 onDataChange() 以获取该值。然后,如果还将启动与服务器的检查,以询问对值的任何更新。如果自上次添加到缓存后服务器上的数据发生更改,它可能随后再次调用 onDataChange()

当您使用addListenerForSingleValueEvent 时会发生什么

当您将单值事件监听器添加到同一位置时:

ref.addListenerForSingleValueEvent(listener);

Firebase 客户端将(与之前的情况一样)立即调用 onDataChange() 以获取本地磁盘缓存中的值。它不会再调用onDataChange() 任何次,即使服务器上的值结果不同。请注意,更新的数据仍将被请求并在后续请求中返回。

How does Firebase sync work, with shared data? 之前已对此进行了介绍

解决方案和解决方法

最好的解决方案是使用addValueEventListener(),而不是单值事件监听器。常规值侦听器将同时获取即时本地事件和来自服务器的潜在更新。

第二种解决方案是使用新的get method(于 2021 年初推出),它没有这种问题行为。请注意,此方法始终尝试首先从服务器获取值,因此需要更长的时间才能完全完成。如果您的值永远不会改变,使用addListenerForSingleValueEvent 可能会更好(但在这种情况下您可能不会出现在此页面上)。

作为一种解决方法,您还可以在使用单值事件侦听器的位置上call keepSynced(true)。这样可以确保数据在发生变化时得到更新,从而极大地提高单值事件侦听器看到当前值的机会。

【讨论】:

感谢完美的插图。但是 keepSynced(true) 和 addValueEventListener 会一直保持打开的连接。与 keepSynced(false) 相比,addListenerForSingleValueEvent 将允许 firebase 在一段时间后断开连接。如何强制一次性手动更新? 多么不方便的行为,它让测试几乎不可能。 调用keepSynced(true)然后使用.addListenerForSingleValueEvent(listener);在数据库更改后第一次打开应用程序时不起作用。在注册更改之前需要打开应用两次。 @Drew 当您在“keepSynced”之后打一个电话时,它实际上可以工作。创建查询,例如设置 keepSynced、addValue 和 onCompleted keepValue false 以避免一直同步。然后刷新数据(没有检查它是否每次都像需要的那样工作)。另一个似乎可行的解决方案是我在这篇文章中的回答 使用 keepSynced 时的警告:它会导致您的带宽大量消耗,并产生相关成本。请参阅以下内容以获得实际解释:pamartinezandres.com/…【参考方案2】:

所以我有一个可行的解决方案。您所要做的就是使用 ValueEventListener 并在 0.5 秒后移除侦听器,以确保您已经在需要时获取了更新的数据。实时数据库有很好的延迟,所以这是安全的。请参阅下面的安全代码示例;

public class FirebaseController 

private DatabaseReference mRootRef;
private Handler mHandler = new Handler();

private FirebaseController() 
    FirebaseDatabase.getInstance().setPersistenceEnabled(true);

    mRootRef = FirebaseDatabase.getInstance().getReference();


public static FirebaseController getInstance() 
    if (sInstance == null) 
        sInstance = new FirebaseController();
    
    return sInstance;

然后是你喜欢使用“addListenerForSingleEvent”的一些方法;

public void getTime(final OnTimeRetrievedListener listener) 
    DatabaseReference ref = mRootRef.child("serverTime");
    ref.addValueEventListener(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            if (listener != null) 
                // This can be called twice if data changed on server - SO DEAL WITH IT!
                listener.onTimeRetrieved(dataSnapshot.getValue(Long.class));
            
            // This can be called twice if data changed on server - SO DEAL WITH IT!
            removeListenerAfter2(ref, this);
        

        @Override
        public void onCancelled(DatabaseError databaseError) 
            removeListenerAfter2(ref, this);
        
    );


// ValueEventListener version workaround for addListenerForSingleEvent not working.
private void removeListenerAfter2(DatabaseReference ref, ValueEventListener listener) 
    mHandler.postDelayed(new Runnable() 
        @Override
        public void run() 
            HelperUtil.logE("removing listener", FirebaseController.class);
            ref.removeEventListener(listener);
        
    , 500);


// ChildEventListener version workaround for addListenerForSingleEvent not working.
private void removeListenerAfter2(DatabaseReference ref, ChildEventListener listener) 
    mHandler.postDelayed(new Runnable() 
        @Override
        public void run() 
            HelperUtil.logE("removing listener", FirebaseController.class);
            ref.removeEventListener(listener);
        
    , 500);

即使他们在处理程序执行之前关闭了应用程序,它仍然会被删除。 编辑:这可以被抽象为使用引用路径作为键和数据快照作为值来跟踪 HashMap 中添加和删除的侦听器。您甚至可以包装一个 fetchData 方法,该方法具有“once”的布尔标志,如果这是真的,它将执行此解决方法以获取一次数据,否则它将正常继续。 不客气!

【讨论】:

【参考方案3】:

您可以创建事务并中止它,然后在线(nline 数据)或离线(缓存数据)时会调用 onComplete

我之前创建的函数只有在数据库连接 lomng 足以进行同步时才起作用。我通过添加超时来解决问题。我将对此进行研究并测试它是否有效。也许以后有空的时候,我会创建android lib并发布它,但到那时它是kotlin中的代码:

/**
     * @param databaseReference reference to parent database node
     * @param callback callback with mutable list which returns list of objects and boolean if data is from cache
     * @param timeOutInMillis if not set it will wait all the time to get data online. If set - when timeout occurs it will send data from cache if exists
     */
    fun readChildrenOnlineElseLocal(databaseReference: DatabaseReference, callback: ((mutableList: MutableList<@kotlin.UnsafeVariance T>, isDataFromCache: Boolean) -> Unit), timeOutInMillis: Long? = null) 

        var countDownTimer: CountDownTimer? = null

        val transactionHandlerAbort = object : Transaction.Handler  //for cache load
            override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) 
                val listOfObjects = ArrayList<T>()
                data?.let 
                    data.children.forEach 
                        val child = it.getValue(aClass)
                        child?.let 
                            listOfObjects.add(child)
                        
                    
                
                callback.invoke(listOfObjects, true)
            

            override fun doTransaction(p0: MutableData?): Transaction.Result 
                return Transaction.abort()
            
        

        val transactionHandlerSuccess = object : Transaction.Handler  //for online load
            override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) 
                countDownTimer?.cancel()
                val listOfObjects = ArrayList<T>()
                data?.let 
                    data.children.forEach 
                        val child = it.getValue(aClass)
                        child?.let 
                            listOfObjects.add(child)
                        
                    
                
                callback.invoke(listOfObjects, false)
            

            override fun doTransaction(p0: MutableData?): Transaction.Result 
                return Transaction.success(p0)
            
        

在代码中,如果设置了超时,那么我设置了计时器,该计时器将通过中止调用事务。即使在离线时也会调用此事务,并将提供在线或缓存数据(在此函数中,此数据被缓存的可能性非常高)。 然后我成功调用事务。 OnComplete 只有在我们从 firebase 数据库得到响应时才会被调用。我们现在可以取消计时器(如果不为空)并将数据发送到回调。

此实现使开发人员 99% 确定数据来自缓存或在线数据。

如果你想让离线速度更快(当显然没有连接数据库时不要愚蠢地等待超时)然后在使用上面的函数之前检查数据库是否连接:

DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() 
  @Override
  public void onDataChange(DataSnapshot snapshot) 
    boolean connected = snapshot.getValue(Boolean.class);
    if (connected) 
      System.out.println("connected");
     else 
      System.out.println("not connected");
    
  

  @Override
  public void onCancelled(DatabaseError error) 
    System.err.println("Listener was cancelled");
  
);

【讨论】:

【参考方案4】:

当 workinkg 启用持久性时,我计算了侦听器收到 onDataChange() 调用的次数,并在 2 次时停止侦听。为我工作,也许有帮助:

private int timesRead;
private ValueEventListener listener;
private DatabaseReference ref;

private void readFB() 
    timesRead = 0;
    if (ref == null) 
        ref = mFBDatabase.child("URL");
    

    if (listener == null) 
        listener = new ValueEventListener() 
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) 
                //process dataSnapshot

                timesRead++;
                if (timesRead == 2) 
                    ref.removeEventListener(listener);
                
            

            @Override
            public void onCancelled(DatabaseError databaseError) 
            
        ;
    
    ref.removeEventListener(listener);
    ref.addValueEventListener(listener);

【讨论】:

如果本地缓存数据与服务器数据相同,onDataChange只会被调用一次,即“ref.removeEventListener(listener);”不会被执行 查看我的工作解决方案***.com/questions/34486417/…

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

离线设备上的 Firebase 云功能

何时在 Firebase 上启用离线功能调用完成块?

Android 中的 Firebase 事件记录离线和在线 [关闭]

使用flutter firebase在线/离线的用户存在

如何为 Firebase 实时数据库上的单个查询启用离线功能?

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