Today we're excited to announce Firebase Hosting integration for Google Cloud’s new Cloud Run service. Cloud Run is a fully managed compute platform that enables developers to run stateless containers that are invocable via HTTP requests in a language and framework of their choosing. Firebase Hosting integration lets you use this architecture as a backend for a web app or microservice in your Firebase project.
Firebase Hosting is already a convenient and secure way to host sites and microservices. It can serve static pages you upload directly and, with the proper configuration in the firebase.json file, direct incoming requests to Cloud Functions for Firebase to serve dynamic content. This workflow is a one stop shop if you don’t mind working in the NodeJS environment. You can already build a fast site with dynamic content that automatically scales horizontally to meet user demand.
Not every developer wants to work with NodeJS though. Many already have large teams with existing knowledge in other languages and frameworks. Languages such as Go, Ruby, and Java have a huge presence in the server market but are currently absent in Firebase’s existing cloud backend solutions.
Leveraging the power of Google’s own experience building infrastructure for Kubernetes and the efforts of the Knative open source project, Google Cloud Platform now lets you deploy stateless servers. The only requirements are that you can generate a docker image able to interact to HTTP requests on the port specified in the $PORT environment variable for Kubernetes and that you respond within 60 seconds for Firebase Hosting. How does this tie into Firebase Hosting though?
If you’re new to Hosting, you may only be aware of static hosting or the free SSL certificates. To facilitate serving dynamic content, rewrites allow you to hit your cloud functions, which we’ve extended to support Cloud Run as well. With a few minor changes to your firebase.json file, you can now point a specific path to your container:
{ "hosting": { "public": "public", "rewrites": [ { "source": "/cloudrun", "run": { "serviceId": "my-awesome-api", // Optional (default is us-central1) "region": "us-central1", } } ] } }
or use wildcards to expose an entire API
{ "hosting": { "public": "public", "rewrites": [ { "source": "/api/**", "run": { "serviceId": "my-awesome-api", // Optional (default is us-central1) "region": "us-central1", } } ] } }
If you have a dynamic site that doesn’t update very frequently, take advantage of Firebase Hosting’s global CDN (content delivery network) to improve your site’s response time. For example, if you’re using ExpressJS and NodeJS, configure the caching behavior using the Cache-Control header like so:
Cache-Control
res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
which caches the results of a request in the browser (max-age) for 5 minutes and in the CDN (s-maxage) for 10 minutes. With properly tuned cache settings, you can have a fast, flexible, dynamically rendered site that doesn’t need to run your server logic every time the user opens the page.
max-age
s-maxage
Unlike Cloud Functions for Firebase, when you use Cloud Run, you can build an image with any combination of languages and frameworks to handle these requests. Ruby developers can easily pull in Sinatra, you can fire up the Spring framework for Java teams, or check out server side Dart using Shelf to serve content. You don’t have to wait for any official language support -- if you can create a docker container, you can make and deploy backend code. Even if you’re working in high performance computing and your engineering team is trained up in Fortran, you can just leverage that existing knowledge to create a web dashboard with Fortran.io without having to wait for any official support from Google or Firebase.
Similar to Cloud Functions, Cloud Run automatically scales your containers horizontally to meet the demands of your users. There’s no need to manage clusters or node pools; you simply use the resources needed at the time to accomplish the task at hand. One tradeoff is that, also like Cloud Functions, Cloud Run images are stateless. However, unlike Cloud Functions, each container can handle up to 80 concurrent users, which can help reduce the frequency of cold starts.
Using Firebase Hosting with Cloud Run, we hope to empower you to build better web apps faster than ever before. Now your frontend and backend developers can truly use a single language, even share a code base. To get started right away, follow our step-by-step guide. Note that Cloud Run exists in the Google Cloud console rather than the Firebase console, but if you have a Firebase project then you already have a Google Cloud Platform project as well.
If you are a user of Firebase Cloud Messaging (FCM), you know that there are many different ways to send messages. You can target an application on a device, or target millions of them through the topics API. FCM APIs tell you if there was a failure during the acceptance of the request by returning an error code and an explanation for the failure, but this only covers the part of the request between you and the FCM backend. What happens after FCM accepts your message request?
In this post, we will look into the Firebase Cloud Messaging infrastructure and how messages are actually delivered to devices.
When FCM returns a successful response to your API request, it simply means that FCM will start trying to deliver this message to the device or to the service responsible for delivering the message to the device (for example: Apple Push Notification service for delivery to iOS devices or Mozilla Push Service for delivery to Firefox browsers). It will try this until:
Note: If the time to live of the pending messages has expired, the above functions will not be triggered.
There are many factors that affect the actual delivery of the message to the device and the amount of information FCM can provide about the delivery.
Note: If you'd like to analyze message delivery events for your project, you can do so by exporting FCM data into BigQuery.
On iOS, there are two ways in which FCM can send a message to a device:
Note: If you'd like to access iOS notification delivery information for display notifications, you can use the FCM Reporting Dashboard. This dashboard leverages Google Analytics for Firebase data sharing features to collect delivery information through Firebase Android and iOS SDKs for display notifications.
See Lifetime of a Message and Understanding Message Delivery if you'd like to learn more about message acceptance and delivery.
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.
Welcome to part 2 of this blog series on using lifecycle-aware Android Architecture Components (LiveData and ViewModel) 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.
ViewModel
DataSnapshot
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:
HotStock
ticker
price
HotStockViewModel
Function
// 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.
Transformations
map()
Deserializer
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())); } } }); } }
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);
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.
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.
onChanged()
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 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.
addSource()
Observer
postValue()
setValue()
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 with a pool of reusable threads (for example) for a job like this.
Executor
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.
/hotstock
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.
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()
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:
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.
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
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.
MyValueEventListener
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.
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.