Lesson Weekend

Now that our application is configured to use our new Firebase database, let's learn how to save data to it. In this lesson we'll create our first Firebase objects and nodes which will allow us to write data to our database.

In later lessons we'll dive into retrieving and displaying data from Firebase, and creating listeners that will watch for changes and sync our data automatically.

Basic Firebase Methods and Format

Let's briefly explore the methods we'll use to create database entries to give you a bit more of an overview, and get some more background on how firebase is structured. Each time we interact with Firebase, we need to create a FirebaseDatabase and DatabaseReference. Why? What does this mean?

Keep the following in mind:

A NoSQL database does not allow us to access data in the same way a SQL database does because of its different structure. Instead of pinpointing data sets that meet certain qualifiers, or locating a specific piece of information through it's relationship to other data, we can look at nodes in our database and pull data from those specific locations. We identify those nodes through references.

firebase-node-structure

Additionally, remember that in Java and therefore Android, everything is an Object (with the exception of primitives and interfaces). Our datatypes of course, but also our activities, our adapters, our services, our views. How we connect and work with databases is no different. So let's start by creating a new FirebaseDatabase object named database. We can call the .getInstance() method to access our database and then write that into a local Object of type FirebaseDatabase.

FirebaseDatabase database = FirebaseDatabase.getInstance();

Now, we can create a reference by calling getReference() on our database instance. ref now refers to our entire database at the root level.

DatabaseReference ref = FirebaseDatabase.getInstance().getReference();

Frequently, these lines are written back-to back:

FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference();

Now we can access data nodes below our root level, by descending down to a child node. This process is somewhat similar to traversing through the DOM with jQuery: We find an element, then locate a secondary element or set of elements in relationship to that parent.

dom_tree_traversal.png

Data in Firebase is stored in JSON-formatted key-value pairs. When we write to the database we use the setValue() method and pass in the value that corresponds to the appropriate child key:

ref.child("<childNodeName>").setValue("<someValue>");

This example creates a new child node with a key of childNodeName and a value of someValue. A node is a general computing term referring to an individual piece of a larger data structure. A child node is simply a node extending from another node.

Update Security Rules

By default, Firebase security rules require users to be authenticated in order to both read and write to the database. We will not be working with user authorization until later this week, so let's alter these rules so we can practice writing and reading to the database without having authorized users.

Navigate to your Firebase app's overview. Select Database from the panel on the left-hand side of the window.

firebase-overview-panel-database

Select the rules tab:

firebase-security-rules-tab

Alter the default rules to reflect the following following:

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

Then, select Publish to save these changes. This will allow any application with the information in our google-services.json file to write to our database. We will change the rules back once we have added user authentication to our app.

Writing to Firebase

Now that we have permissions to read and write to Firebase, let's write our first piece of data. We're currently saving the zip code a user enters into MyRestaurants in Android's shared preferences. Let's practice writing to Firebase by implementing code that will save this data to Firebase instead. Later on, when we have more data to work with, we'll switch back to saving the user's zip code to shared preferences. As you follow along, simply comment out any code referring to shared preferences.

First, we'll need to define the child name of the Firebase node we'd like to save this information to in Constants.java. We'll call our node searchedLocation, and it will contain data for zip codes a user has searched for:

Constants.java
public final class Constants {
    ...
    public static final String FIREBASE_CHILD_SEARCHED_LOCATION = "searchedLocation";
}

Here, we define a constant called FIREBASE_CHILD_SEARCHED_LOCATION, and set it equivalent to the string "searchedLocation". This will be the key of our node's key-value pair in Firebase.

Now, we'll save user-entered zip codes into the searchedLocation node by adding the following to our MainActivity, and commenting out our previous code saving this data to shared preferences:

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

    private DatabaseReference mSearchedLocationReference;

    @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);

        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.setValue(location);
    }

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

}

Here, we add the instance of our SearchedLocations DatabaseReference, instantiating it in our onCreate() method passing in our FIREBASE_CHILD_SEARCHED_LOCATION as an argument.

Then, we call setValue() on this Firebase object, providing the user-submitted zip code as an argument. Remember, nodes are specific locations in your database, and they're also key-value pairs. By calling setValue() we're providing a value that corresponds to the key of searchedLocation.

You may have noticed that we have not yet defined or created a searchedLocation node in our Firebase dashboard. However, when the method above runs, Firebase will create this node for us if it cannot find a pre-existing node of the same name.

The onClick() method will now call saveLocationToFirebase() instead of saving this location into shared preferences. Each time the method is called a new zip code will be saved to the searchedLocation node in Firebase.

Now if we run the app and enter a new zip code we should see it appear in our Firebase dashboard under the Data tab.

first-searched-location-saved-in-firebase-dashboard

Unique Node IDs

However, if we enter a new location the previous location will be overwritten. To prevent this, let's call the push() method before setting the value. This will ensure each new entry is added to the node under a unique, randomly generated id called a push id:

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

After making the changes above, we can run the app, enter a few new locations and see that our searchedLocation node now contains multiple locations with their own unique id:

firebase-dashboard-multiple-location-entries

Since we're (at least temporarily) saving the user's searched zip codes in Firebase, let's make the following quick alteration to RestaurantListActivity to ensure our Yelp API requests are being made with the user's most recent search, not an older zip code stored in shared preferences:

RestaurantListActivity.java
public class RestaurantListActivity extends AppCompatActivity {
//    private SharedPreferences mSharedPreferences;
//    private String mRecentAddress;

    @Bind(R.id.recyclerView) RecyclerView mRecyclerView;

    private RestaurantListAdapter mAdapter;
    public ArrayList<Restaurant> mRestaurants = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_restaurants);
        ButterKnife.bind(this);

        Intent intent = getIntent();
        String location = intent.getStringExtra("location");

        getRestaurants(location);

//        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
//        mRecentAddress = mSharedPreferences.getString(Constants.PREFERENCES_LOCATION_KEY, null);
//
//        if (mRecentAddress != null) {
//            getRestaurants(mRecentAddress);
//        }
    }
...

Here, we're simply commenting out any reference to shared preferences, and the code that previously made a Yelp API request using mRecentAddress. Instead, we're making a request with our getRestaurants() method using location like we were previously. Again, we'll revisit using shared preferences later on.

Firebase Data Structure

Unlike SQL, Firebase is not a relational database and it's structure is not tabular. Instead, Firebase data is stored in JSON, which has a nested structure and can be represented using a tree. The Data dashboard in your Firebase dashboard is simply a visual representation of your JSON database tree.

In fact, we can export our data by clicking on the overflow menu from the database toolbar:

firebase-overflow-menu

and then selecting the "Export Data" button:

firebase-export-json

And download our database's JSON, which currently looks something like this:

{
  "searchedLocation" : {
    "-KGTi05yPv_0tjM2Jiqz" : "97204",
    "-KGTi5m6uamM1IkHfXhC" : "97201"
  }
}

The database for our MyRestaurants app will eventually look something like this:

MyRestaurants Database Structure

Our Firebase database will contain key-value pairs for each restaurant a user saves. The value associated with a restaurant is a map of 10 child key-value pairs.

Each collection of data, called a node, also has a path associated with it. For example, the path to access the phone number for the restaurant with the push id KFB1RHsfuEVcpORnzaH (Snack Shack, in this case) would be DatabaseReference phoneRef = FirebaseDatabse.getInstance().getReference("restaurants").child("KFB1RHsfuEVcpORnzaH").child("phone");. Like a file path, these data references are unique paths to access a specific piece or group of data.

Final Thoughts - How do I efficiently structure my database?

After your exposure to Objects and JSON in your JavaScript course, you may be compelled to structure your database by nesting individual key/value pairs in a list, nesting that list within another list, nesting that list as a value of a key, and so on.

This structure has some benefits -- it is intuitively readable, and, at first glance, has little data repetition. After all, being DRY and not having repetitive code is one of hallmarks of an efficient developer. But this approach is not recommended. It is actually preferable to have nodes reference each other, and, in some cases, even duplicate certain data. Restructuring your data to prevent nesting is called denormalizing the data to achieve a flat data structure.

In Firebase, a ValueEventListener can be tasked with downloading information from a specific node. But when this is completed, the node and all of its child nodes are also downloaded and stored in the app, even if you only want one specific subset of that data. This unnecessarily taxes the server with requests for data through nested listeners, and the performance of both the server and the app can plummet due to inefficient data retrieval.

Therefore, when building an app that is even slightly complex, always denormalize your data, creating as flat of a hierarchy as possible - even if this means some data repetition or seeming redundancy.

For more information, we recommend checking out this and this video from Udacity's Firebase Essentials for Android series and reading the Structure Your Database section of the Firebase docs.


Example GitHub Repo for MyRestaurants

Terminology


  • Node: a general computing term referring to an individual piece of a larger data structure.

  • Child node: A node extending from another node.

  • Push ID: A unique, randomly generated id associated with each entry by the node, and created by Firebase.

Examples


  • Each time we interact with Firebase we need to create a FirebaseDatabase and DatabaseReference. We can call the .getInstance() method to access our database and then using the database object, we can call the getReference() method to get a specific reference within our database:
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference();
  • Data in Firebase is stored in JSON-formatted key-value pairs. When we write to the database we use the setValue() method and pass in the value that corresponds to the appropriate child key:
ref.child("<childNodeName>").setValue("<someValue>");

Tips


  • When writing data, if Firebase cannot find the node specified it will create one automatically.

Additional Resources


  • For more information, check out the Data Structure section of Firebase's Android Guides.