Lesson Weekend

Now that users can successfully save their favorite restaurants to Firebase, let's make sure they can view the list of restaurants they've saved. To do this, we'll integrate another open source library called FirebaseUI-Android.

Similar to our RestaurantListActivity, we will display each saved restaurant using the custom restaurant_list_item layout. It is therefore possible for us to reuse our RestaurantListAdapter and RestaurantListViewHolder. However, when we delete data from our database, the RestaurantListAdapter won't know it needs to update the view!

We've already integrated listeners that automatically return updated data to our application; let's make sure our user interface is similarly dynamic and also automatically updates when changes occur. In order to address this, we'll use FirebaseUI's FirebaseRecyclerAdapter to map our Restaurant data from Firebase to Android.

Firebase RecyclerView Setup

First off, take a moment to glance over the general FirebaseUI's README and the firebase-ui-database docs.

To install this tool we'll add the following to our app's dependencies:

build.gradle (Module: app)
dependencies {
    ...
    compile 'com.firebaseui:firebase-ui-database:0.4.1'
}

Firebase ViewHolder

Before we create our FirebaseRecyclerAdapter, let's create a new ViewHolder. It will look similar to our RestaurantViewHolder, but because FirebaseUI handles the ViewHolder construction for us, we won't have as many customization options.

First, create a new Java class called FirebaseRestaurantViewHolder.java in the adapters sub-package. Then, add the following code:

FirebaseRestaurantViewHolder.java
public class FirebaseRestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    private static final int MAX_WIDTH = 200;
    private static final int MAX_HEIGHT = 200;

    View mView;
    Context mContext;

    public FirebaseRestaurantViewHolder(View itemView) {
        super(itemView);
        mView = itemView;
        mContext = itemView.getContext();
        itemView.setOnClickListener(this);
    }

    public void bindRestaurant(Restaurant restaurant) {
        ImageView restaurantImageView = (ImageView) mView.findViewById(R.id.restaurantImageView);
        TextView nameTextView = (TextView) mView.findViewById(R.id.restaurantNameTextView);
        TextView categoryTextView = (TextView) mView.findViewById(R.id.categoryTextView);
        TextView ratingTextView = (TextView) mView.findViewById(R.id.ratingTextView);

        Picasso.with(mContext)
                .load(restaurant.getImageUrl())
                .resize(MAX_WIDTH, MAX_HEIGHT)
                .centerCrop()
                .into(restaurantImageView);

        nameTextView.setText(restaurant.getName());
        categoryTextView.setText(restaurant.getCategories().get(0));
        ratingTextView.setText("Rating: " + restaurant.getRating() + "/5");
    }

    @Override
    public void onClick(View view) {
        final ArrayList<Restaurant> restaurants = new ArrayList<>();
        DatabaseReference ref = FirebaseDatabase.getInstance().getReference(Constants.FIREBASE_CHILD_RESTAURANTS);
        ref.addListenerForSingleValueEvent(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
                    restaurants.add(snapshot.getValue(Restaurant.class));
                }

                int itemPosition = getLayoutPosition();

                Intent intent = new Intent(mContext, RestaurantDetailActivity.class);
                intent.putExtra("position", itemPosition + "");
                intent.putExtra("restaurants", Parcels.wrap(restaurants));

                mContext.startActivity(intent);
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
            }
        });
    }
}
  • Just like in our RestaurantViewHolder, we add static variables to hold the width and height of our images for Picasso.

  • We then add member variables to hold the view and context which we set in our constructor.

  • We also implement the View.OnClickListener interface and set the click listener on our itemView.

  • In our bindRestaurant() method, we first bind the views and then set the image and text views.

  • Finally, in the onClick() method, we create a singleValueEventListener to grab out the current list of restaurants from Firebase which we pass along to the RestaurantDetailActivity in the form of an intent extra. We will need this ArrayList when constructing an instance of the RestaurantDetailFragment.

SavedRestaurantListActivity

Before we can hook our app up with the corresponding data in Firebase, we first need to create a place to display it. Let's start by creating a new activity called SavedRestaurantListActivity.java. We can reuse the activity_restaurants.xml layout since all we need is a RecyclerView so when creating this new activity, make sure to uncheck the Generate Layout File box:

dont-generate-layout-in-android-studio

We can now set the appropriate content view, bind the RecyclerView, and set the FirebaseRecyclerAdapter from the FirebaseUI library:

SavedRestaurantListActivity.java
public class SavedRestaurantListActivity extends AppCompatActivity {
    private DatabaseReference mRestaurantReference;
    private FirebaseRecyclerAdapter mFirebaseAdapter;

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

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

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

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

    private void setUpFirebaseAdapter() {
        mFirebaseAdapter = new FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>
                (Restaurant.class, R.layout.restaurant_list_item, FirebaseRestaurantViewHolder.class, 
                    mRestaurantReference) {

            @Override
            protected void populateViewHolder(FirebaseRestaurantViewHolder viewHolder, 
                Restaurant model, int position) {
                viewHolder.bindRestaurant(model);
            }
        };
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setAdapter(mFirebaseAdapter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mFirebaseAdapter.cleanup();
    }
}
  • First, we initialize our DatabaseReference, FirebaseRecyclerAdapter, and RecyclerView member variables.

  • We then pass in the activity_restaurants layout into the setContentView() method to display the correct layout.

  • Next, we set the mRestaurantReference using the "restaurants" child node key from our Constants class.

  • We then create a method to set up the FirebaseAdapter which takes the model class, the list item layout, the view holder, and the database reference as parameters.

  • Inside of the populateViewHolder() method, we call the bindRestaurant() method on our viewHolder to set the appropriate text and image with the given restaurant.

  • We then set the adapter on our RecyclerView.

  • Finally, we need to clean up the FirebaseAdapter. When the activity is destroyed, we need to call cleanup() on the adapter so that it can stop listening for changes in the Firebase database.

Add Navigation to SavedRestaurantListActivity

Finally, we just need to create a button to navigate to this new activity. Add the following button to your activity_main.xml layout:

activity_main.xml
...
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="My Saved Restaurants"
        android:id="@+id/savedRestaurantsButton"
        android:background="@color/colorAccent"
        android:textColor="@color/colorTextIcons"
        android:visibility="visible"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"/>
...

And alter your existing "Find Restaurants" button to make room for the new "Saved Restaurants" button:

activity_main.xml
...
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Find Restaurants"
        android:id="@+id/findRestaurantsButton"
        android:background="@color/colorAccent"
        android:textColor="@color/colorTextIcons"
        android:visibility="visible"
        android:layout_above="@+id/savedRestaurantsButton"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginBottom="10dp" />
...

We'll also bind the new "Saved Restaurants" button's view, call setOnClickListener() upon it in the onCreate() method of MainActivity, and add an additional if statement to the onClick() override to handle creating and beginning a new intent when the Saved Restaurants button is selected:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    @Bind(R.id.savedRestaurantsButton) Button mSavedRestaurantsButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mSavedRestaurantsButton.setOnClickListener(this);
    }

    ...

    @Override
    public void onClick(View v) {

        ...

        if (v == mSavedRestaurantsButton) {
            Intent intent = new Intent(MainActivity.this, SavedRestaurantListActivity.class);
            startActivity(intent);
        }
    }
}

Now we can run our app, click on “My Saved Restaurants” and see our list of saved restaurants!


The FirebaseRecyclerAdapter sets the childValueEventListener on the location we specified in our reference.

To test that the view automatically updates when an item is deleted, let’s open the Database tab in our Firebase app and delete a restaurant by selecting the red "X" near its node:

delete-node-from-firebase-dashboard

The view in our emulator should update immediately if everything was set up properly; the deleted restaurant should immediately be removed from the list of saved restaurants.


Example GitHub Repo for MyRestaurants

Terminology


  • FirebaseUI-Android: An open-source library that connects common user interface elements to the FIrebase API.

  • FirebaseRecyclerAdapter: A RecyclerAdapter specially-made for integration with Firebase.

Examples


Example GitHub Repo for MyRestaurants

Additional Resources