Hey, welcome to part 3 in this series about using lifecycle-aware Android Architecture Components with Firebase Realtime Database. In part 1, we started with a simple Activity that uses database listeners to keep its UI fresh as data changes in the database. We converted that to use LiveData and ViewModel to remove the boilerplate of dealing with the listeners during the Activity lifecycle. Then, in part 2, we completely refactored away all mention of Realtime Database from the Activity, and implemented a performance enhancement. This optimization uses MediatorLiveData and some threading, for the case where data manipulation might be too expensive operation to perform on the main thread.
MediatorLiveData
There's one more optimization that can be applied in the code. It could have a large impact on performance, depending on how much data your database listeners are receiving. It has to do with how our FirebaseQueryLiveData implementation deals with the database listener during its onActive() and onInactive() methods. Here it is again:
FirebaseQueryLiveData
onActive()
onInactive()
public class FirebaseQueryLiveData extends LiveData<DataSnapshot> { private static final String LOG_TAG = "FirebaseQueryLiveData"; private final Query query; private final MyValueEventListener listener = new MyValueEventListener(); public FirebaseQueryLiveData(Query query) { this.query = query; } public FirebaseQueryLiveData(DatabaseReference ref) { this.query = ref; } @Override protected void onActive() { query.addValueEventListener(listener); } @Override protected void onInactive() { query.removeEventListener(listener); } private class MyValueEventListener implements ValueEventListener { @Override public void onDataChange(DataSnapshot dataSnapshot) { setValue(dataSnapshot); } @Override public void onCancelled(DatabaseError databaseError) { Log.e(LOG_TAG, "Can't listen to query " + query, databaseError.toException()); } } }
The key detail to note here is that a database listener is added during onActive() and removed during onInactive(). The Activity that makes use of FirebaseQueryLiveData executes this code during its onCreate():
onCreate()
HotStockViewModel viewModel = ViewModelProviders.of(this).get(HotStockViewModel.class); LiveData<DataSnapshot> liveData = viewModel.getDataSnapshotLiveData(); liveData.observe(this, new Observer<DataSnapshot>() { @Override public void onChanged(@Nullable DataSnapshot dataSnapshot) { if (dataSnapshot != null) { // update the UI here with values in the snapshot } } });
The observer here follows the lifecycle of the Activity. LiveData considers an observer to be in an active state if its lifecycle is in the STARTED or RESUMED state. The observer transitions to an inactive state if its lifecycle is in the DESTROYED state. The onActive() method is called when the LiveData object has at least one active observer, and the onInactive() method is called when the LiveData object doesn't have any active observers. So, what happens here when the Activity is launched, then goes through a configuration change (such as a device reorientation)? The sequence of events (when there is a single UI controller observing a FirebaseQueryLiveData) is like this:
Activity
LiveData
I've bolded the steps that deal with the database listener. You can see here the Activity configuration change caused the listener to be removed and added again. These steps spell out the cost of a second round trip to and from the Realtime Database server to pull down all the data for the second query, even if the results didn't change. I definitely don't want that to happen, because LiveData already retains the latest snapshot of data! This extra query is wasteful, both of the end user's data plan, and and counts against the quota or the bill of your Firebase project.
There's no easy way to change the way that the LiveData object becomes active or inactive. But we can make some guesses about how quickly that state could change when the Activity is going through a configuration change. Let's make the assumption that a configuration change will take no more than two seconds (it's normally much faster). With that, one strategy could add a delay before FirebaseQueryLiveData removes the database listener after the call to onInactive(). Here's an implementation of that, with a few changes and additions to FirebaseQueryLiveData:
private boolean listenerRemovePending = false; private final Handler handler = new Handler(); private final Runnable removeListener = new Runnable() { @Override public void run() { query.removeEventListener(listener); listenerRemovePending = false; } }; @Override protected void onActive() { if (listenerRemovePending) { handler.removeCallbacks(removeListener); } else { query.addValueEventListener(listener); } listenerRemovePending = false; } @Override protected void onInactive() { // Listener removal is schedule on a two second delay handler.postDelayed(removeListener, 2000); listenerRemovePending = true; }
Here, I'm using a Handler to schedule the removal of the database listener (by posting a Runnable callback that performs the removal) on a two second delay after the LiveData becomes inactive. If it becomes active again before those two seconds have elapsed, we simply eliminate that scheduled work from the Handler, and allow the listener to keep listening. This is great for both our users and our wallets!
Handler
Runnable
Are you using lifecycle-aware Android Architecture components along with Firebase in your app? How's it going? Join the discussion of all things Firebase on our Google group firebase-talk.