AnalyticsIntroduction

With Google I/O 2016 in the books, there were some exciting announcements about Firebase. To get an overview of what is new with Firebase take a look at the Google I/O presentation. Firebase also unveiled a new integrated console to access all of your data and services. In this post we'll be looking at the Firebase Realtime Database. The demo app referenced in this post can be found on Github.

The Firebase Realtime Database provides your app with a cloud-hosted NoSQL database. Data is synched across connected clients as soon as data is changed. Firebase also supports offline mode, writing database changes to a local database before sending them off to the Firebase server for synchronization with other devices. Firebase also provides client SDKs for web, Android, and iOS as well as a REST API for integrations with your own server.

The demo app that will be used is a simple note taking app that shares the list of notes across all users. The app allows the user to create new notes, edit existing notes, and delete individual notes. In this post we'll look at integrating the Firebase Realtime Database as our backing source for the list of notes.

Setup

To begin using Firebase you will need to create a free account at https://firebase.google.com/.

  • Create a project
  • Choose add "Add Firebase to your Android app"
  • Enter the app package name (com.antoinecampbell.firebase.demo)
  • Enter the SHA-1 fingerprint from your debug certificate (instructions)
  • Download the generated google-services.json file and place it into the app/ directory
  • Update the rules for your database to allow reads and writes from all users, as this demo will not cover Firebase Authentication.
    {
     "rules": {
     ".read": "true",
     ".write": "true"
     }
    }
  • The demo app is ready to be launched
  • Optionally, you may bootstrap your database with some records by importing the notes-export.json file to your database

App Setup

The first step to getting the app ready to use Firebase is to add Google Play Services to the project. In the root build.gradle file add the following line to your dependencies (/build.gradle):

classpath 'com.google.gms:google-services:3.0.0'
Next, add the Firebase Realtime Database SDK to your app level build.gradle file dependencies (/app/build.gradle):
compile 'com.google.firebase:firebase-database:9.4.0'
Then, apply the Google Play Services plugin at the bottom of your app level build.gradle file (/app/build.gradle):
apply plugin: 'com.google.gms.google-services'
Finally, we will override the application class to enable Firebase's offline mode before any database references are used (FirebaseDemoApplication.java):
@Override
public void onCreate() {
 super.onCreate();
 FirebaseDatabase.getInstance().setPersistenceEnabled(true);
}

Retrieving Data

To retrieve data from Firebase Realtime Database we need to query the path containing the list of notes, in this case the path is "notes". The data and its children will be returned when queried, so all stored notes will be returned from the querying the path "notes". The only way to query is asynchronously via listeners which return a snapshot of the data at the time of the event. When a listener is attached a snapshot of the data is sent. The listener will be called whenever data on the path being watched is changed, including its children.

One thing to keep in mind when querying data from Firebase Realtime Database is that it's not like your typical database. In the efforts to be as fast as possible, some of the more advanced querying options are not available to you such as; partial text searching, object references, and joining. One solution the Firebase team recommends to deal with these limitations is to denormalize your data. The query below returns the value of the "notes" path from the database, which is the list of stored notes. Also, the Firebase SDK provides built-in deserialization functionality to parse the data into your POJO.

DatabaseReference database = FirebaseDatabase.getInstance().getReference();
...
database.child("notes").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
 List notes = new ArrayList<>();
 for (DataSnapshot noteDataSnapshot : dataSnapshot.getChildren()) {
 Note note = noteDataSnapshot.getValue(Note.class);
 notes.add(note);
 }
 adapter.updateList(notes);
}
...
});

There may be cases where you do not want to receive constant updates on data changes and instead simply fetch the data once. To do so, use the addListenerForSingleValueEvent() method instead. In offline situations, data would be returned from the local database if present.

Saving Data

In the demo application new notes are "pushed" onto the notes path. In Firebase, the concept of pushing creates a unique identifier for the new item adding it to the end of the specified path. Data in Firebase Realtime Database is stored as JSON and pushing creates a nested element beneath the specified path. The JSON below shows all the data that is stored for two notes.

{
 "notes" : {
 "-KOCEiiQduWwBMxYUf4x" : {
 "description" : "test 1",
 "title" : "Test 1",
 "uid" : "-KOCEiiQduWwBMxYUf4x"
 },
 "-KOCEkP0itzpoGmLrVQh" : {
 "description" : "Test 2",
 "title" : "Test 2",
 "uid" : "-KOCEkP0itzpoGmLrVQh"
 },
 "-KOCEmo7DUrh7u_qyMQ_" : {
 "description" : "Test 3",
 "title" : "Test 3",
 "uid" : "-KOCEmo7DUrh7u_qyMQ_"
 }
}

You will notice that the key for each note is duplicated within the note itself. The reason being when Firebase returns the data from the notes path, accessing the children of the path will not have a unique identifier as part of object when deserialized into a POJO. So, during the saving process, the note object is set with the unique identifier returned from the push before saving the contents of the note. Essentially, when saving a note, the identifier for the note is created before the content of the note is saved. This allows for simple deserialization, having the unique identifier as part of the note, removing the need to set the identifier after deserialization.

fab.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View view) {
 Note note = new Note();
 note.setUid(database.child("notes").push().getKey());
 note.setTitle(titleTextView.getText().toString());
 note.setDescription(descriptionTextView.getText().toString());
 database.child("notes").child(note.getUid()).setValue(note);
 finish();
 }
});

Updating Data

Updating your data with Firebase is just like saving, except there is no need to do a push to generate a new key.

fab.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View view) {
 note.setTitle(titleTextView.getText().toString());
 note.setDescription(descriptionTextView.getText().toString());
 database.child("notes").child(note.getUid()).setValue(note);
 finish();
 }
});

Optionally, a completion listener can be passed to the setValue() method to be notified when the data saves to Firebase servers. However, the listener would not be called when in offline mode, so anything UI-related that needed to happen after a save may not be executed. In the demo, this completion listener is omitted, and we know the data will eventually get synchronized when the user is back online.

Deleting Data

Much like saving data, removing data from Firebase is rather simple. We remove the path associated with the note, in this case "notes/". The code below is from the MainActivity in the demo app.

@Override
public void itemRemoved(int position) {
 Note note = adapter.getItem(position);
 adapter.removeItem(position);
 database.child("notes").child(note.getUid()).removeValue();
}

Recall that the "notes" path is being watched by the listener and deleting a note will trigger a call to onDataChange() passing the new list of notes to the adapter. However, if the adapter receives a new list of notes the removal animation will not get a chance to complete before notifiyDataSetChanged() is called. To keep the UI running smoothly, calls to updateList() are essentially ignored if the data change was made on the user's device. If the change was made externally, by another device or from the Firebase console, the app will just refresh the content.

public void updateList(List notes) {
 // Allow recyclerview animations to complete normally if we already know about data changes
 if (notes.size() != this.notes.size() || !this.notes.containsAll(notes)) {
 this.notes = notes;
 notifyDataSetChanged();
 }
}

Conclusion

Using Firebase Realtime Database as your backing datastore can allow you to get your app up and running very quickly. With SDKs available for the web, Android, iOS, and a REST API it will help make the integration smooth. When considering the other services, the Firebase team announced at Google I/O 2016 it makes the platform very appealing as a service provider.

Some things to consider when looking at Firebase Realtime Database is the structure of your data:

  • Your data should lend itself to being represented in a NoSQL environment. 2+-way relationships can make switching to NoSQL difficult.
  • Your application or website needs realtime updates of data.
  • The structure of your data changes frequently or is not defined. Data like this is ideal for NoSQL.
  • Your application can live without powerful aggregation functions and advanced querying.

Firebase Realtime Database sacrifices functionality for speed. You may not be able to replace everything, but some critical paths of your application can certainly be enhanced.

About the Author

Antoine CampbellAntoine Campbell is a full-stack developer in our Charlotte office with a passion for Android development. Antoine also serves as a programming mentor for a local FRC robotics team.