Firebase 离线功能和 addListenerForSingleValueEvent

Firebase Offline Capabilities and addListenerForSingleValueEvent

每当我将 addListenerForSingleValueEventsetPersistenceEnabled(true) 一起使用时,我只能设法获取 DataSnapshot 的本地离线副本,而 NOT 更新后的 DataSnapshot 来自服务器。

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

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

更新 (2021)一个新方法调用 (get on Android and getData on iOS),它实现了您将要执行的行为like want:它首先尝试从服务器获取最新值,只有在无法到达服务器时才回退到缓存。使用持久侦听器的建议仍然适用,但至少有一个更清晰的选项,即使您启用了本地缓存,也可以一次性获取数据。


持久性如何工作

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()。 If 然后还将启动与服务器的检查,以请求对值的任何更新。如果服务器上的数据自上次添加到缓存后发生变化,它可能随后再次调用onDataChange()

当您使用 addListenerForSingleValueEvent

时会发生什么

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

ref.addListenerForSingleValueEvent(listener);

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

之前在 How does Firebase sync work, with shared data?

中对此进行了介绍

解决方案和解决方法

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

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

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

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

我之前创建的函数只有在数据库获得足够的连接以进行同步时才有效。我通过添加超时来解决问题。我将致力于此并测试它是否有效。也许以后有空的时候,我会创建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");
  }
});

在启用持久性的情况下工作时,我计算了侦听器收到对 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);
}

所以我有一个可行的解决方案。您所要做的就是使用 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 方法,它有一个布尔标志为“一次”,如果这是真的,它会执行这个变通方法来获取数据一次,否则它会照常继续。 不客气!