This year at Google I/O 2017, the Android platform team announced the availability of Android Architecture Components, which provides libraries that help you design robust, testable, and maintainable apps. Among all the tools it offers, I'm particularly impressed by the way it helps you manage the lifecycle of your app's activities and fragments - a common concern for Android developers.
In this blog series, I'll explore how these libraries can work together with the Firebase Realtime Database SDK to help architect your app. The way client apps read data from Realtime Database is through listeners that get called with updates to data as it's written. This allows you to easily keep your app's UI fresh with the latest data. It turns out that this model of listening to database changes works really well Android Architecture Components. (Also note that the information here applies equally well to Firestore, which also delivers data updates to client apps in real time.)
Android apps that use Realtime Database often start listening for changes during the onStart() lifecycle method, and stop listening during onStop(). This ensures that they only receive changes while an Activity or Fragment is visible on screen. Imagine you have an Activity that displays the ticker and most recent price of today's hot stock from the database. The Activity looks like this:
onStart()
onStop()
Activity
Fragment
public class MainActivity extends AppCompatActivity { private static final String LOG_TAG = "MainActivity"; private final DatabaseReference ref = FirebaseDatabase.getInstance().getReference("/hotstock"); private TextView tvTicker; private TextView tvPrice; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvTicker = findViewById(R.id.ticker); tvPrice = findViewById(R.id.price); } @Override protected void onStart() { super.onStart(); ref.addValueEventListener(listener); } @Override protected void onStop() { ref.removeEventListener(listener); super.onStop(); } private ValueEventListener listener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // update the UI here with values in the snapshot String ticker = dataSnapshot.child("ticker").getValue(String.class); tvTicker.setText(ticker); Float price = dataSnapshot.child("price").getValue(Float.class); tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price)); } @Override public void onCancelled(DatabaseError databaseError) { // handle any errors Log.e(LOG_TAG, "Database error", databaseError.toException()); } }; }
It's pretty straightforward. A database listener receives updates to the stock price located at /hotstock in the database, and the values are placed into a couple TextView objects. For very simple cases like this, there's not a problem. But if this app becomes more complex, there's a couple immediate issues to be aware of:
/hotstock
TextView
1. Boilerplate
There's a lot of standard boilerplate here for defining a DatabaseReference at a location in the database and managing its listener during onStart() and onStop(). The more listeners involved, the more boilerplate code will clutter this code. And a failure to remove all added listeners could result in data and memory leaks - one simple mistake could cost you money and performance.
DatabaseReference
2. Poor testability and readability
While the effect of the code is straightforward, it's difficult to write pure unit tests that verify the logic, line by line. Everything is crammed into a single Activity object, which becomes difficult to read and manage.
Digging into the libraries provided by Architecture Components, you'll find there are two classes in particular that are helpful to address the above issues: ViewModel and LiveData. If you haven't read about how these work, please take a moment to read about ViewModel and LiveData to learn about them. I'll also be extending LiveData, so take a look there as well. It's important to understand the way they interact with each other, in addition to the LifecycleOwner (e.g. an Activity or Fragment) that hosts them.
ViewModel
LiveData
LifecycleOwner
Extending LiveData with Firebase Realtime Database
LiveData is an observable data holder class. It respects the lifecycle of Android app components, such as activities, fragments, or services, and only notifies app components that are in an active lifecycle state. I'll use it here to listen to changes to a database Query or DatabaseReference (note that a DatabaseReference itself is a Query), and notify an observing Activity of those changes so it can update its UI. These notifications come in the form of DataSnapshot objects that you'd normally expect from the database listener. Here's a LiveData extension that does exactly that:
Query
DataSnapshot
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() { Log.d(LOG_TAG, "onActive"); query.addValueEventListener(listener); } @Override protected void onInactive() { Log.d(LOG_TAG, "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()); } } }
With FirebaseQueryLiveData, whenever the data from the Query given in the constructor changes, MyValueEventListener triggers with a new DataSnapshot, and it notifies any observers of that using the setValue() method on LiveData. Notice also that MyValueEventListener is managed by onActive() and onInactive(). So, whenever the Activity or Fragment associated with this LiveData object is on screen (in the STARTED or RESUMED state), the LiveData object is "active", and the database listener will be added.
FirebaseQueryLiveData
MyValueEventListener
setValue()
onActive()
onInactive()
The big win that LiveData gives us is the ability to manage the database listener according to the state of the associated Activity. There's no possibility of a leak here because FirebaseQueryLiveData knows exactly when and how to set up and tear down its business. Note that we can reuse this class for all kinds of Firebase queries. This FirebaseQueryLiveData class is a very reusable class!
Now that we have a LiveData object that can read and distribute changes to the database, we need a ViewModel object to hook that up to the Activity. Let's take a look at how to do that.
Implementing a ViewModel to manage FirebaseQueryLiveData
ViewModel implementations contain LiveData objects for use in a host Activity. Because a ViewModel object survives Activity configuration changes (e.g. when the user reorients their device), its LiveData member object will be retained as well. The lifetime of a ViewModel with respect to its host Activity can be illustrated like this:
Here's a ViewModel implementation that exposes a FirebaseQueryLiveData that listens to the location /hotstock in a Realtime Database:
public class HotStockViewModel extends ViewModel { private static final DatabaseReference HOT_STOCK_REF = FirebaseDatabase.getInstance().getReference("/hotstock"); private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF); @NonNull public LiveData<DataSnapshot> getDataSnapshotLiveData() { return liveData; } }
Note that this ViewModel implementation exposes a LiveData object. This allows the Activity that uses HotStockViewModel to actively observe any changes to the underlying data under /hotstock in the database.
HotStockViewModel
Using LiveData and ViewModel together in an Activity
Now that we have LiveData and ViewModel implementations, we can make use of them in an Activity. Here's what the Activity from above now looks like after refactoring to use LiveData and ViewModel:
public class MainActivity extends AppCompatActivity { private TextView tvTicker; private TextView tvPrice; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvTicker = findViewById(R.id.ticker); tvPrice = findViewById(R.id.price); // Obtain a new or prior instance of HotStockViewModel from the // ViewModelProviders utility class. 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 String ticker = dataSnapshot.child("ticker").getValue(String.class); tvTicker.setText(ticker); Float price = dataSnapshot.child("price").getValue(Float.class); tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price)); } } }); } }
It's about 20 lines of code shorter now, and easier to read and manage!
During onCreate(), it gets a hold of a HotStockViewModel instance using this bit of code:
HotStockViewModel viewModel = ViewModelProviders.of(this).get(HotStockViewModel.class);
ViewModelProviders is a utility class from Architecture Components that manages ViewModel instances according to the given lifecycle component. In the above line, the resulting HotStockViewModel object will either be newly created if no ViewModel of the named class for the Activity exists, or obtained from a prior instance of the Activity before a configuration change occurred.
ViewModelProviders
With an instance of HotStockViewModel, the Activity response changes to its LiveData by simply attaching an observer. The observer then updates the UI whenever the underlying data from the database changes.
So, what's the advantage to doing things this way?
ValueEventListener
If you look at the new Activity implementation, you can see that most of the details of working with Firebase Realtime Database have been moved out of the way, into FirebaseQueryLiveData, except for dealing with the DataSnapshot. Ideally, I'd like to remove all references to Realtime Database altogether from the Activity so that it doesn't have to know or care where the data actually comes from. This is important if I ever want to migrate to Firestore - the Activity won't have to change much, if at all.
There's another subtle issue with the fact that each configuration change removes and re-adds the listener. Each re-add of the listener effectively requires another round trip with the server to fetch the data again, and I'd rather not do that, in order to avoid consuming the user's limited mobile data. Enabling disk persistence helps, but there's a better way (stay tuned to this series for that tip!).
We'll solve these two problems in future posts, so stay tuned here to the Firebase Blog by following @Firebase on Twitter! You can click through to part 2 right here.