Lesson Thursday

Now that our app allows each user their own personal account, let's make sure each user's "Saved Restaurants" list is correctly associated with them. This will ensure that when a user logs in, they see their "Saved Restaurants" .

In this lesson we'll alter the structure of our database to ensure that each restaurant a user saves is attributed to their account. To do this, we'll ensure each user has a unique node, we'll add a pushId attribute to our Restaurant objects, and alter our existing Firebase query to return only a user's own data.

Review: Data Structure in MyRestaurants

Before we begin, let's review some ground we have already covered where data structure in Firebase is concerned.

When a user adds a restaurant to their list, we want to save the restaurant object to a user's own dedicated node in our database. To do this, we'll create a node for each user under their account's uid property, since it's guaranteed to be unique. And, while we'll continue to save restaurants under a unique push ID, we'll also capture this ID and save it as an attribute in each restaurant object.

This will result in a database that looks like this:

Firebase Data Structure

Saving User-Specific Data

Restaurant Push IDs

We'll begin by adding a pushId attribute to our Restaurant class. As detailed in the image above, we'll save each restaurant's unique pushId in our database. Next week, when we add functionality to delete restaurants from "Saved Restaurants" we'll need a method of identifying the specific restaurant to remove from a user's node. This pushId attribute will allow our app to pinpoint the exact restaurant object to delete.

Let's add this new attribute now:

Restaurant.java

@Parcel
public class Restaurant {
    String name;
    String phone;
    String website;
    double rating;String imageUrl;
    List<String> address = new ArrayList<>();
    double latitude;
    double longitude;
    List<String> categories = new ArrayList<>();
    private String pushId;

    public Restaurant() {}

    public Restaurant(String name, String phone, String website,
                      double rating, String imageUrl, ArrayList<String> address,
                      double latitude, double longitude, ArrayList<String> categories) {
        this.name = name;
        this.phone = phone;
        this.website = website;
        this.rating = rating;
        this.imageUrl = getLargeImageUrl(imageUrl);
        this.address = address;
        this.latitude = latitude;
        this.longitude = longitude;
        this.categories = categories;
    }

    public String getName() {
        return name;
    }

    public String getPhone() {
        return phone;
    }

    public String getWebsite() {
        return  website;
    }

    public double getRating() {
        return rating;
    }

    public String getImageUrl() { return imageUrl;}

    public String getLargeImageUrl(String imageUrl) {
        String largeImageUrl = imageUrl.substring(0, imageUrl.length() - 6).concat("o.jpg");
        return largeImageUrl;
    }

    public List<String> getAddress() {
        return address;
    }

    public double getLatitude() {
        return latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public List<String> getCategories() {
        return categories;
    }

    public String getPushId() {
        return pushId;
    }

    public void setPushId(String pushId) {
        this.pushId = pushId;
    }
}

Here, we've declared a pushId of type String, and created getPushId() and setPushId() methods to retrieve or assign the relevant ID to an object. The pushId is not included in the constructor because it won't be provided when the object is first created. Since Firebase creates this value, we cannot assign it to the object until later on, when we're ready to save to Firebase.

Saving Objects

Now that we have added user authentication to our app, we need to update how we save restaurants to our Firebase database. In order to associate restaurants to a given user, we need to add the restaurants by the user's UID.

Let's start by grabbing the currently authenticated user's UID in our RestaurantDetailFragment since that's where we are saving our restaurant objects. Add the following to our mSaveRestaurantsButton click listener:

RestaurantDetailFragment.java
public class RestaurantDetailFragment extends Fragment implements View.OnClickListener {
    ...

    @Override
    public void onClick(View v) {
        ...
        if (v == mSaveRestaurantButton) {
            FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
            String uid = user.getUid();

            ...
        }
    }
}
  • To get the currently authorized user, we simply need to get the current instance of the FirebaseAuth object and then call the getCurrentUser() method.
  • We can then grab their UID by calling the getUid() method on our instance of the FirebaseUser object.

Now we just need to update our restaurantRef variable so that it points to the correct location in the Firebase database to save restaurants to the user with the returned UID:

RestaurantDetailFragment.java
...
if (v == mSaveRestaurantButton) {
            FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
            String uid = user.getUid();
            DatabaseReference restaurantRef = FirebaseDatabase
                    .getInstance()
                    .getReference(Constants.FIREBASE_CHILD_RESTAURANTS)
                    .child(uid);
            ...
        }
...
  • We use the child() method to create a node within the restaurants node to store the given user's list of restaurants.

Finally, let's add the pushID of the restaurant to be saved before setting the value at given reference:

RestaurantDetailFragment.java
...
if (v == mSaveRestaurantButton) {
            FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
            String uid = user.getUid();

            DatabaseReference restaurantRef = FirebaseDatabase
                    .getInstance()
                    .getReference(Constants.FIREBASE_CHILD_RESTAURANTS)
                    .child(uid);

            DatabaseReference pushRef = restaurantRef.push();
            String pushId = pushRef.getKey();
            mRestaurant.setPushId(pushId);
            pushRef.setValue(mRestaurant);

            Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show();
        }
...
  • We call push() on the new child node. This prompts Firebase to create a unique push ID. This is the ID we will assign to the restaurant's new pushId attribute.

  • We collect this ID by calling getKey(), and assign it a variable name.

  • We call setPushId(), providing the Firebase push ID as an argument, to define therestaurant's pushId.

  • Finally, we call setValue() to save the restaurant object in the specified node in Firebase, and display a Toast.

If we run our app we should be able to log in, save several restaurants, and see these restaurants appear under the appropriate uid in our database.

Maintaining Database Consistency

However, we do have one small problem. Now that we've added a new attribute to restaurants and changed where we save them, any new restaurants we save will conform to our new standards, but any restaurants added before these changes will have an entirely different data structure.

We always want our databases to be as consistent as possible. Therefore, we'll need to delete all existing restaurants from Firebase by selecting the red "X" next to the restaurants node:

deleting-restaurants-from-firebase

Moving forward, all restaurants saved will be saved in the appropriate data structure, with the necessary attributes, and our database will have consistent structure that follows best practices!

Retrieving User-Specific Data

Now that we've organized our database appropriately, we need to alter our SavedRestaurantListActivity in order to display the "Saved Restaurants" list associated with the user currently logged in. Let's start by grabbing the currently authorized user's UID in onCreate(). We can do this by adding the following two lines:

SavedRestaurantListActivity.java
public class SavedRestaurantListActivity extends AppCompatActivity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
        String uid = user.getUid();
        ...
    }

    ...
}

Next, we need to update our existing mRestaurantReference variable so that it points to the correct location in the database, just like we did in our mSaveRestaurantButton click listener in the RestaurantDetailFragment:

SavedRestaurantListActivity.java
...
@Override
    protected void onCreate(Bundle savedInstanceState) {
         ...
        FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
        String uid = user.getUid();

        mRestaurantReference = FirebaseDatabase
                .getInstance()
                .getReference(Constants.FIREBASE_CHILD_RESTAURANTS)
                .child(uid);
        ...
    }

The completed onCreate() method should look like this:

SavedRestaurantListActivity.java
...
  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_restaurants);
        ButterKnife.bind(this);

        FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
        String uid = user.getUid();

        mRestaurantReference = FirebaseDatabase
                .getInstance()
                .getReference(Constants.FIREBASE_CHILD_RESTAURANTS)
                .child(uid);

        setUpFirebaseAdapter();
    }
...

Now, if we launch the app again, save a few restaurants, and then navigate to the SavedRestaurantsActivity, we should see that each user can only see their own individual "Saved Restaurants" list.

Database Rules

With authorized users fully established and able to save restaurants to their personalized node, let's update the security settings in our Firebase Database so that only authorized users can read and write to it. Navigate to the Rules tab in the Database and change the rules to the following:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

For more information on Firebase Database rules, read Get Started with Database Rules.


Example GitHub Repo for MyRestaurants

Tips


Firebase database rules allowing only authenticated users to write to the database:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

Examples


Additional Resources