Welcome to part 2 of this blog series on using lifecycle-aware Android Architecture Components ([LiveData](https://developer.android.com/topic/libraries/architecture/livedata.html)
and [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html)
) along with Firebase Realtime Database to implement more robust and testable apps! In the first part, we saw how you can use LiveData
and ViewModel
to simplify your Activity code, by refactoring away most of the implementation details of Realtime Database from an Activity. However, one detail remained: the Activity was still reaching into the DataSnapshot
containing the stock price. I’d like to remove all traces of the Realtime Database SDK from my Activity so that it’s easier to read and test. And, ultimately, if I change the app to use Firestore instead of Realtime Database, I won’t even have to change the Activity code at all.
Here’s a view of the data in the database:
and here’s the code that reads it out of DataSnapshot
and copies into a couple TextViews:
// update the UI with values from 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));
The Realtime Database SDK makes it really easy to convert a DataSnapshot into a JavaBean style object. The first thing to do is define a bean class whose getters and setters match the names of the fields in the snapshot:
public class HotStock {
private String ticker;
private float price;
public String getTicker() {
return ticker;
}
public void setTicker(String ticker) {
this.ticker = ticker;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public String toString() {
return "{HotStock ticker=" + ticker + " price=" + price + "}";
}
}
Then I can tell the SDK to automatically perform the mapping like this:
HotStock stock = dataSnapshot.getValue(HotStock.class)
After that line executes, the new instance of HotStock
will contain the values for ticker
and price
. Using this handy line of code, I can update my HotStockViewModel
implementation to perform this conversion by using a transformation. This allows me to create a LiveData
object that automatically converts the incoming DataSnapshot
into a HotStock
. The conversion happens in a Function
object, and I can assemble it like this in my ViewModel
:
// This is a LiveData<DataSnapshot> from part 1
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF);
private final LiveData<HotStock> hotStockLiveData =
Transformations.map(liveData, new Deserializer());
private class Deserializer implements Function<DataSnapshot, HotStock> {
@Override
public HotStock apply(DataSnapshot dataSnapshot) {
return dataSnapshot.getValue(HotStock.class);
}
}
@NonNull
public LiveData<HotStock> getHotStockLiveData() {
return hotStockLiveData;
}
The utility class Transformations
provides a static method map()
that returns a new LiveData
object given a source LiveData
object and a Function
implementation. This new LiveData
applies the Function
to every object emitted by the source, then turns around and emits the output of the Function
. The Deserializer
function here is parameterized by the input type DataSnapshot
and the output type HotStock
, and it has one simple job - deserialize a DataSnapshot
into a HotStock
. Lastly, we’ll add a getter for this new LiveData
that emits the transformed HotStock
objects.
With these additions, the application code can now choose to receive updates to either DataSnapshot
or HotStock
objects. As a best practice, ViewModel
objects should emit objects that are fully ready to be consumed by UI components, so that those components are only responsible for displaying data, not processing data. This means that HotStockViewModel
should be doing all the preprocessing required by the UI layer. This is definitely the case here, as HotStock
is fully ready to consume by the Activity that’s populating the UI. Here’s what the entire Activity looks like now:
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);
HotStockViewModel hotStockViewModel =
ViewModelProviders.of(this).get(HotStockViewModel.class);
`
LiveData<HotStock> hotStockLiveData = hotStockViewModel.getHotStockLiveData();
`
hotStockLiveData.observe(this, new Observer() {
@Override
public void onChanged(@Nullable HotStock hotStock) {
if (hotStock != null) {
// update the UI here
with values in the snapshot
tvTicker.setText(hotStock.getTicker());
tvPrice.setText(String.format(Locale.getDefault(), "%.2f",
hotStock.getPrice()));
}
}
});
}
}
All the references to Realtime Database objects are gone now, abstracted away behind HotStockViewModel
and LiveData
! But there’s still one potential problem here.
What if a LiveData
transformation is expensive?
All LiveData
callbacks to onChanged()
run on the main thread, as well as any transformations. The example I’ve given here is very small and straightforward, and I wouldn’t expect there to be performance problems. But when the Realtime Database SDK deserializes a DataSnapshot
to a JavaBean type object, it uses reflection to dynamically find and invoke the setter methods that populate the bean. This can become computationally taxing as the quantity and size of the objects increase. If the total time it takes to perform this conversion is over 16ms (your budget for a unit of work on the main thread), Android starts dropping frames. When frames are dropped, it no longer renders at a buttery-smooth 60fps, and the UI becomes choppy. That’s called “jank”, and jank makes your app look poor. Even worse, if your data transformation performs any kind of I/O, your app could lock up and cause an ANR.
If you have concerns that your transformation can be expensive, you should move its computation to another thread. That can’t be done in a transformation (since they run synchronously), but we can use something called [MediatorLiveData](https://developer.android.com/topic/libraries/architecture/livedata.html#merge_livedata)
instead. MediatorLiveData
is built on top of a map transform, and allows us to observe changes other LiveData
sources, deciding what to do with each event. So I’ll replace the existing transformation with one that gets initialized in the no-arg constructor for HotStockViewModel
from part 1 of this series:
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF);
private final MediatorLiveData<HotStock> hotStockLiveData = new MediatorLiveData<>();
public HotStockViewModel() {
// Set up the MediatorLiveData to convert DataSnapshot objects into HotStock objects
hotStockLiveData.addSource(liveData, new Observer<DataSnapshot>() {
@Override
public void onChanged(@Nullable final DataSnapshot dataSnapshot) {
if (dataSnapshot != null) {
new Thread(new Runnable() {
@Override
public void run() {
hotStockLiveData.postValue(dataSnapshot.getValue(HotStock.class));
}
}).start();
} else {
hotStockLiveData.setValue(null);
}
}
});
}
Here, we see that addSource()
is being called on the MediatorLiveData
instance with a source LiveData
object and an Observer
that gets invoked whenever that source publishes a change. During onChanged()
, it offloads the work of deserialization to a new thread. This threaded work is using postValue()
to update the MediatorLiveData
object, whereas the non-threaded work when (dataSnapshot is null) is using setValue()
. This is an important distinction to make, because postValue()
is the thread-safe way of performing the update, whereas setValue()
may only be called on the main thread.
NOTE: I don’t recommend starting up a new thread like this in your production app. This is not an example of “best practice” threading behavior. Optimally, you might want to use an [Executor](https://developer.android.com/reference/java/util/concurrent/Executor.html)
with a pool of reusable threads (for example) for a job like this.
There’s still room for improvement!
Now that we’ve removed Realtime Database objects from the Activity and accounted for the performance of the transformation from DataSnapshot
to HotStock
, there’s still another performance improvement to make here. When the Activity goes through a configuration change (such as a device reorientation), the FirebaseQueryLiveData
object will remove its database listener during onInactive()
, then add it back during onActive()
. While that doesn’t seem like a problem, it’s important to realize that this will cause another (unnecessary) round trip of all data under /hotstock
. I’d rather leave the listener added and save the user’s data plan in case of a reorientation. So, in the next part of this series, I’ll look at a way to make that happen.
I hope to see you next time, and be sure to follow @Firebase on Twitter to get updates on this series! You can click through to part 3 right here.