The problem: the layout in my MainActivity is generated before I have a chance to finish calling the firebase to restore app data. If I rotate the screen, thus causing onCreate to run again in the MainActivity, everything is generated just fine.
In my app, I have a custom implementation of the Application class, which makes a bunch of calls to Firebase in order to restore data / make sure data is always in sync. However, instead of having just a few ValueEventListeners with a ton of children underneath, I have around 20 ValueEventListeners. This is to prevent my app from syncing nearly the entire database every time the user generates a tiny bit of data, and to avoid conflicts that can happen when data is manipulated asynchronously. Interestingly, the ValueEventListeners don't actually fetch their data in the order that they're coded in, so I can't set a bool to true when the last one is done.
I'm wondering if there's a simple way to detect whether the firebase reads are all done, other than placing some code at the end of every single Listener and performing an operation that does this manually. I've looked at the methods available in the Application class, as well as Firebase, and so far I haven't found anything that works.
some code from my Application class:
public class CompassApp extends Application {
... then inside the Application's onCreate:
// Fetching Data from DB.
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference dbRef = database.getReference();
// Current User Data
dbRef.child("currentAppData").child("workingOut").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
activeFirebaseConnections += 1;
// Stops executing method if there is no data to retrieve
if (!dataSnapshot.exists()) {
return;
}
workingOut = dataSnapshot.getValue(boolean.class);
activeFirebaseConnections -= 1;
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.e(TAG, "Firebase read of sleepDebt failed");
}
});
dbRef.child("currentAppData").child("sleeping").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
activeFirebaseConnections += 1;
// Stops executing method if there is no data to retrieve
if (!dataSnapshot.exists()) {
return;
}
sleeping = dataSnapshot.getValue(boolean.class);
activeFirebaseConnections -= 1;
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.e(TAG, "Firebase read of sleepDebt failed");
}
});
(and so on... the rest is just more ValueEventListeners)
Rulesets must obey two size limits: a 256 KB limit on the size of the ruleset text source published from the Firebase console or from the CLI using firebase deploy . a 250 KB limit on the size of the compiled ruleset that results when Firebase processes the source and makes it active on the back-end.
One of the main problems with it, is limited querying capabilities. Realtime database provides no way to filter capabilities, because the whole DB is a huge JSON file, which makes it pretty difficult to make complex queries. Another point to consider also relates to Firebase Realtime DB and its data modeling.
256 MB from the REST API; 16 MB from the SDKs. The total data in each write operation should be less than 256 MB. Multi-path updates are subject to the same size limitation.
Automatic Scaling Additionally, the API functions of firebase are designed in order to scale linearly with the size of data being synchronized. It handles the scaling operations. Your application will scale from its first user to its first million user without any change in the code.
I'm currently using a utility class that I created for kicking off multiple loads of data. It will trigger a final Task when all of the listeners are complete. They key here is clever use of the new Task facility for asynchronous programming provided by Play Services. You could probably do all this using some other async framework, but Tasks come for free with Play Services and are used in other parts of Firebase, so you may as well learn to use them. :-)
I gave a talk at Google I/O 2016 about tasks. Here is a link to a video that jumps directly to the relevant part of that session.
public class FirebaseMultiQuery {
private final HashSet<DatabaseReference> refs = new HashSet<>();
private final HashMap<DatabaseReference, DataSnapshot> snaps = new HashMap<>();
private final HashMap<DatabaseReference, ValueEventListener> listeners = new HashMap<>();
public FirebaseMultiQuery(final DatabaseReference... refs) {
for (final DatabaseReference ref : refs) {
add(ref);
}
}
public void add(final DatabaseReference ref) {
refs.add(ref);
}
public Task<Map<DatabaseReference, DataSnapshot>> start() {
// Create a Task<DataSnapsot> to trigger in response to each database listener.
//
final ArrayList<Task<DataSnapshot>> tasks = new ArrayList<>(refs.size());
for (final DatabaseReference ref : refs) {
final TaskCompletionSource<DataSnapshot> source = new TaskCompletionSource<>();
final ValueEventListener listener = new MyValueEventListener(ref, source);
ref.addListenerForSingleValueEvent(listener);
listeners.put(ref, listener);
tasks.add(source.getTask());
}
// Return a single Task that triggers when all queries are complete. It contains
// a map of all original DatabaseReferences originally given here to their resulting
// DataSnapshot.
//
return Tasks.whenAll(tasks).continueWith(new Continuation<Void, Map<DatabaseReference, DataSnapshot>>() {
@Override
public Map<DatabaseReference, DataSnapshot> then(@NonNull Task<Void> task) throws Exception {
task.getResult();
return new HashMap<>(snaps);
}
});
}
public void stop() {
for (final Map.Entry<DatabaseReference, ValueEventListener> entry : listeners.entrySet()) {
entry.getKey().removeEventListener(entry.getValue());
}
snaps.clear();
listeners.clear();
}
private class MyValueEventListener implements ValueEventListener {
private final DatabaseReference ref;
private final TaskCompletionSource<DataSnapshot> taskSource;
public MyValueEventListener(DatabaseReference ref, TaskCompletionSource<DataSnapshot> taskSource) {
this.ref = ref;
this.taskSource = taskSource;
}
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
snaps.put(ref, dataSnapshot);
taskSource.setResult(dataSnapshot);
}
@Override
public void onCancelled(DatabaseError databaseError) {
taskSource.setException(databaseError.toException());
}
}
}
The way you use it is like this. Determine all the DatabaseReference
s you need to load data from and store them in members of your Activity. Then, during your activity onStart()
, pass them all into an instance of FirebaseMultiQuery and call start()
on it. It will return a Task that generates a Map of DatabaseReference to the DataSnapshot it will generate. With that Task in hand, register a final listener that will trigger when all the data is loaded:
firebaseMultiQuery = new FirebaseMultiQuery(dbRef1, dbRef2, dbRef3, ...);
final Task<Map<DatabaseReference, DataSnapshot>> allLoad = firebaseMultiQuery.start();
allLoad.addOnCompleteListener(activity, new AllOnCompleteListener());
And in your onStop()
, don't forget to call stop()
on it to make sure everything is shut down properly:
firebaseMultiQuery.stop();
The listener triggered by the completion of the task that receives all the data in a map can look something like this:
private class AllOnCompleteListener implements OnCompleteListener<Map<DatabaseReference, DataSnapshot>> {
@Override
public void onComplete(@NonNull Task<Map<DatabaseReference, DataSnapshot>> task) {
if (task.isSuccessful()) {
final Map<DatabaseReference, DataSnapshot> result = task.getResult();
// Look up DataSnapshot objects using the same DatabaseReferences you passed into FirebaseMultiQuery
}
else {
exception = task.getException();
// log the error or whatever you need to do
}
// Do stuff with views
updateUi();
}
}
It's perhaps worth noting that Firebase Database calls that write data also return Tasks that you can listen on for completion. Also, Firebase Storage uses Tasks, and so does Firebase Authentication.
The code here may not currently work with a Firebase Database Query that is not a simple named reference into a node in your database. It depends on whether they are hashed in a way that's consistent for keying into a HashMap.
The code above is still somewhat experimental, but I haven't had any problems with it so far. It seems like a lot of code, but if you do a lot of concurrent loading, it's really handy.
I was facing the same problem with my app. After a long research on how to check if firebase is done retrieving data, I found this solution.
So when you have multiple event listeners in your app. Firebase doesn't execute this listeners one by one in sequence.
The Value Event Listener is always executed at last.
So if you want to execute something when all the data is retrieved, you can call a ValueEventListener and execute a method which loads all your views inside it.
So use rootRef.addListenerForSingleValueEvent()
and inside its onDataChanged()
execute what you want to execute after data is retrieved from Firebase.
This worked for me, Hope it works for you too
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With