Lesson Weekend

Now that we can successfully save data from our MyRestaurants application into Firebase, how do we retrieve that data when we need it? Remember, again, that unlike in a SQL database we cannot pinpoint data in the same way we can with a RDBMS - instead, we identify data through changes to a certain node, or reference point. In this lesson we'll learn how to create and implement listeners that will deliver updated database information directly to our app.

Listeners and Snapshots

Firebase utilizes listeners to watch for changes in a specified node. It is similar to an event handler in the sense that a code is triggered based on a certain circumstance. In our case, whenever changes in that node's data occur, the listener automatically provides the application updated data, called a snapshot. The application can then use information from the snapshot to update the UI.

This differs drastically from the request/response model we've used in the past, such as requesting information from Yelp's API. The request/response format requires our application make new request threads to regularly check for new data from the data source. When using listeners, Firebase only sends updates to our app when data changes.

Similar to OKHttp, the Firebase SDK handles all threading and asynchronicity for us, too! We just need to determine what data we'd like to listen to, and what to do when the listener returns new data.

Types of Listeners in Firebase

There are several types of Listeners for Firebase, and each Listener type has a different kind of callback that is triggered when the listener is activated.

ValueEventListener: A ValueEventListener listens for data changes to a specific location in your database - i.e a node. ValueEventListener has one event callback method, onDataChange() to read a static snapshot of the contents at a given path, as they existed at the time of the event. This method is triggered once when the listener is attached and again every time the data, including children, changes. The event callback is passed a snapshot containing all data at that location, including child data. If there is no data, the snapshot returned is null.

If the Event can not be completed, a second callback method, onCancelled() is called.

ChildEventListener: A ChildEventListener listens for changes to the children of a specific database reference, for example the root node of a database. It has the following callback methods:

onCancelled(DatabaseError error) 
// This method will be triggered in the event that this listener either failed at the server, or is removed as a result of the security and Firebase rules.

onChildAdded(DataSnapshot snapshot, String previousChildName)
 // triggered when a new child is added to the location to which this listener was added.

onChildChanged(DataSnapshot snapshot, String previousChildName)
 //  triggered when the data at a child location has changed.

onChildMoved(DataSnapshot snapshot, String previousChildName) 
// triggered when a child location's priority changes.

onChildRemoved(DataSnapshot snapshot) 
// triggered when a child is removed from the location to which this listener was added.

For your own apps that require data persistence, it is a good idea to read through the additional documentation on ValueEventListener (read both the guide and reference) and ChildEventListener read the reference

Make sure you understand the difference between event listener types and when they are used - alone, or in combination.

Creating a ValueEventListener

Let’s add a ValueEventListener to MyRestaurants. We'll include code that logs data from our searchedLocations node in Firebase whenever changes occur (such as when we add a new location):

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private DatabaseReference mSearchedLocationReference;

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        mSearchedLocationReference = FirebaseDatabase
                .getInstance()
                .getReference()
                .child(Constants.FIREBASE_CHILD_SEARCHED_LOCATION);//pinpoint location node

        mSearchedLocationReference.addValueEventListener(new ValueEventListener() { //attach listener

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) { //something changed!
                for (DataSnapshot locationSnapshot : dataSnapshot.getChildren()) {
                    String location = locationSnapshot.getValue().toString();
                    Log.d("Locations updated", "location: " + location); //log
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) { //update UI here if error occurred.

            }
        });

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

     ...

...

Here, we define a Firebase DatabaseReference object called mSearchedLocationReference and pass it the child key of the searchedLocations node (as covered in this lesson). Then, we call addValueEventListener() on our mSearchedLocationReference object to attach a new ValueEventListener (which we provide as a parameter).

As mentioned above, ValueEventListeners have two methods that must be overridden; onDataChange() and onCancelled():

To recap:

  • onDataChange() is called whenever data at the specified node changes. Such as adding a new zip code. It will return a dataSnapshot object, which is essentially a read-only copy of the Firebase state.

  • onCancelled() is called if the listener is unsuccessful for any reason. We won't add any code here right now, but could in the future.

In our onDataChange method we'll snag the values returned in the dataSnapshot, loop through each of the children with the getChildren() method, and print their values to the logcat. Other methods we can call on a dataSnapshot include .child() and .getKey().

Each time a new location is added it will be saved to our searchedLocations node, which will trigger our ValueEventListener to provide an updated list of locations to our app. When our app receives these locations, it will log them to the logcat. Here, we can see several locations logged to the logcat:

logging-event-listener-data

And, if we search for yet another zip code, we can see there are now more locations logged. The listener recognized changes in the searchedLocations node, and automatically sent the updated information to our app. Pretty cool!

logging-event-listener-data-again

Removing Event Listeners

When creating valueEventListeners, it's important to consider how they should be handled while the user is not actively interacting with our application. In its current state, when a user navigates away from MyRestaurants, our listener will continue to listen for changes in Firebase. As you can imagine, continually listening will eat away battery life, and eventually can cause memory leaks.

Let's remove our listener when the user quits interacting with the activity. To do this, we'll declare and attach a variable name to the listener itself, so we may instruct our app to destroy it when the user quits the activity:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...

    private ValueEventListener mSearchedLocationReferenceListener;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        mSearchedLocationReference = FirebaseDatabase
                .getInstance()
                .getReference()
                .child(Constants.FIREBASE_CHILD_SEARCHED_LOCATION);

        mSearchedLocationReferenceListener = mSearchedLocationReference.addValueEventListener(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                for (DataSnapshot locationSnapshot : dataSnapshot.getChildren()) {
                    String location = locationSnapshot.getValue().toString();
                    Log.d("Locations updated", "location: " + location);
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }

        });

    ...

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSearchedLocationReference.removeEventListener(mSearchedLocationReferenceListener);
    }

  ...

}

In the code above, we declare the member variable mSearchedLocationReferenceListener and assign it to the event listener. Then, we override the activity's onDestroy() method that automatically runs when the activity is halted. We explicitly instruct our app to remove the listener from our Firebase node when the activity is destroyed.

Note that the onDestroy() method is an override for the activity, not the listener. It is defined in the top level of the class, not nested within the addValueEventListener() block.

The completed activity should look like this:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//    private SharedPreferences mSharedPreferences;
//    private SharedPreferences.Editor mEditor;

    private DatabaseReference mSearchedLocationReference;

    private ValueEventListener mSearchedLocationReferenceListener;

    @Bind(R.id.findRestaurantsButton) Button mFindRestaurantsButton;
    @Bind(R.id.locationEditText) EditText mLocationEditText;
    @Bind(R.id.appNameTextView) TextView mAppNameTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        mSearchedLocationReference = FirebaseDatabase
                .getInstance()
                .getReference()
                .child(Constants.FIREBASE_CHILD_SEARCHED_LOCATION);

        mSearchedLocationReferenceListener = mSearchedLocationReference.addValueEventListener(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                for (DataSnapshot locationSnapshot : dataSnapshot.getChildren()) {
                    String location = locationSnapshot.getValue().toString();
                    Log.d("Locations updated", "location: " + location);
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }

        });

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Typeface ostrichFont = Typeface.createFromAsset(getAssets(), "fonts/ostrich-regular.ttf");
        mAppNameTextView.setTypeface(ostrichFont);

//        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
//        mEditor = mSharedPreferences.edit();

        mFindRestaurantsButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(v == mFindRestaurantsButton) {
            String location = mLocationEditText.getText().toString();

            saveLocationToFirebase(location);

//            if(!(location).equals("")) {
//                addToSharedPreferences(location);
//            }

            Intent intent = new Intent(MainActivity.this, RestaurantListActivity.class);
            intent.putExtra("location", location);
            startActivity(intent);
        }
    }

    public void saveLocationToFirebase(String location) {
        mSearchedLocationReference.push().setValue(location);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSearchedLocationReference.removeEventListener(mSearchedLocationReferenceListener);
    }

//    private void addToSharedPreferences(String location) {
//        mEditor.putString(Constants.PREFERENCES_LOCATION_KEY, location).apply();
//    }

}


Example GitHub Repo for MyRestaurants

Terminology


  • Listeners: Watch for changes in a specified node. Whenever changes occur, listeners automatically provide the application updated data.

  • Snapshot: Updated data from a node. Usually provided to the application by a listener, when changes to the node occur.

Tips


  • To prevent consuming unnecessary battery power and memory, remove valueEventListeners manually when the user is no longer interacting with the application. This can be done by calling the removeEventListener() method on the Firebase database reference in the activity's onDestroy() override.

Examples


Defining a new valueEventListener and removing it in onDestroy() when a user quits an activity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...

    private ValueEventListener mSearchedLocationReferenceListener;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        mSearchedLocationReference = FirebaseDatabase
                .getInstance()
                .getReference()
                .child(Constants.FIREBASE_CHILD_SEARCHED_LOCATION);

        mSearchedLocationReferenceListener = mSearchedLocationReference.addValueEventListener(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                ...
                //   code here is executed when Firebase recognizes a change made to the 
                //   node being listened to, and provides a new dataSnapshot. 
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                //   code here is executed if the listener is unsuccessful 
                //   for any reason. 
            }
        });

    ...

    @Override
    protected void onDestroy() {     
        //    defined in 'top level' of activity, not nested within another block. 
        //    code here is executed when the user quits the activity. 

        super.onDestroy();
        mSearchedLocationReference.removeEventListener(mSearchedLocationReferenceListener);
    }
  ...

Additional Resources