Lesson Weekend

Note: In some cases, adding the RecyclerView dependency library can lead to version incompatibility errors. This is a known issue. If you experience errors after adding the RecyclerView library, see the Addressing Version Incompatibility section at the end of this lesson.

So far, we only used Android's built-in ArrayAdapters to display lists of information in an activity. We've altered those ArrayAdapters to be able to pass additional data, and we have made a simple custom adapter to work with a simple layout. Often though, we'll want to display more than just a single string, and we'll want to use our own layout. Because the objects we are passing around and parsing into custom layouts are about to get more complex, we will spend some time learning about a powerful new tool to process information into a repeating layout: A RecyclerView.

Required Reading

Begin by reading CodePath's article Using the Recycler View.

RecyclerView

The RecyclerView is a newer Android ViewGroup object meant to render any adapter-based View. It's similar to a ListView, but with many updated features, including the ability to implement both horizontal and vertical lists. (This will come in handy later, when we add functionality to display content horizontally or vertically depending on the device orientation).

RecyclerView Requirements

  • To use a RecyclerView widget, you must also include its corresponding RecyclerView.Adapter and LayoutManager.
    • A LayoutManager is responsible for positioning individual item views inside the RecyclerView. The LayoutManager knows the size of the layout, and can compute how much space needs to be reserved to show the optimum amount of entries.

There are three built-in LayoutManager options: * LinearLayoutManager: Displays items in a vertical or horizontal scrolling list. * GridLayoutManager: Displays items in a grid. * StaggeredGridLayoutManager: Displays items in a more staggered grid. * Every RecyclerView must also be backed by a model - this means that it can parse or lay out a specific Object.

RecyclerView.Adapter

The RecyclerView.Adapter, much like the built-in Android ArrayAdapter, will populate the data into the RecyclerView. It also converts a Java object into an individual list item View to be inserted and displayed to the user.

Let's look at this image one more time, as we are about to build something very similar:

app_object_overview

To be able to bring the above sketch to life, we'll need the following code pieces.

Requirements

  • The RecyclerView.Adapter requires a ViewHolder. A ViewHolder is an object that stores multiple Views inside the tag field of the Layout so they can be immediately loaded, and you don't have to find them by id repeatedly. This also improves application performance.
  • The RecyclerView.Adapter has three primary methods: onCreateViewHolder(), onBindViewHolder(), and getItemCount().
    • onCreateViewHolder() inflates an XML layout and returns a ViewHolder.
    • onBindViewHolder() sets the various information in the list item View through the ViewHolder. This is the moment when the data from our model gets associated, aka "bound" to our view.
    • getItemCount() simply returns the number of items the RecyclerView will be responsible for listing, so that it knows how many list item views it will need to recycle.

Consider this visual depictions of how the RecyclerView works: 06-recyclerviewer-adapter.png

and

04-view-recycling.png

Enough theory. Let's get coding.

Adding RecyclerView to MyRestaurants

First, we'll add the RecyclerView support library:

build.gradle (Module: app)
dependencies {
   ...
    compile 'com.android.support:recyclerview-v7:+'
}

We'll then add the RecyclerView widget to our activity_restaurants.xml:

activity_restaurants.xml
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="16dp"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:paddingTop="16dp"
    tools:context=".ui.RestaurantsActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recyclerView"/>

</RelativeLayout>

Next we'll create a layout to define the appearance of each restaurant list item. Create a new layout resource file called restaurant_list_item.xml and add the following code to create our list item placeholder:

restaurant_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

        <ImageView
            android:layout_width="130dp"
            android:layout_height="100dp"
            android:id="@+id/restaurantImageView"
            android:src="@drawable/waffles"
            android:scaleType="centerCrop"/>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:background="#ffffff"
        android:layout_height="match_parent"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/restaurantNameTextView"
            android:textSize="20dp"
            android:textStyle="bold"
            android:text="Restaurant Name"
            android:textColor="@color/colorPrimary"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="One Cuisine Type"
            android:id="@+id/categoryTextView"
            android:layout_below="@+id/restaurantNameTextView"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:textStyle="italic"/>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rating"
            android:id="@+id/ratingTextView"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:textColor="@color/colorAccent"/>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>
  • We wrap the ratingTextView in a RelativeLayout inside of it's containing vertical LinearLayout so that it will display nicely on the bottom right corner of the list item.

  • To do this, we must specify that the relative layout's height and width match the remaining space allotted after the restaurant and cuisine type TextViews using the match_parent value.

Note: Feel free to use any placeholder image you want. Here, we are using an image in our Drawable folder called "waffles".

Creating Custom Adapters

Next, we'll create our custom adapter. Create a new package called adapters. Inside it, make a new Java class called RestaurantListAdapter.

Our custom RestaurantListAdapter class will need to extend the RecyclerView.Adapter class. We'll also include a constructor:

adapters/RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    private ArrayList<Restaurant> mRestaurants = new ArrayList<>();
    private Context mContext;

    public RestaurantListAdapter(Context context, ArrayList<Restaurant> restaurants) {
        mContext = context;
        mRestaurants = restaurants;
    }
}

We will need mContext to create our ViewHolder, and mRestaurants to calculate the item count, which informs the RecyclerView how many individual list item Views it will need to recycle.

RecyclerView View Holders

We also know our RecyclerView adapter will require a ViewHolder. We can create this as an inner-class here within our RestaurantListAdapter class. An inner-class, also sometimes referred to as a nested class is simply a class that resides within another class. They have all functionality of a non-nested class, but with limited scope. They also have full access to the class in which they are nested.

Our RestaurantViewHolderinner class will extend the RecyclerView.ViewHolder class, use ButterKnife to bind all views in the layout, and include a method called bindRestaurant() that will set the contents of the layout's TextViews to the attributes of a specific restaurant:

adapters/RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    private ArrayList<Restaurant> mRestaurants = new ArrayList<>();
    private Context mContext;

    public RestaurantListAdapter(Context context, ArrayList<Restaurant> restaurants) {
        mContext = context;
        mRestaurants = restaurants;
    }

    public class RestaurantViewHolder extends RecyclerView.ViewHolder {
        @Bind(R.id.restaurantImageView) ImageView mRestaurantImageView;
        @Bind(R.id.restaurantNameTextView) TextView mNameTextView;
        @Bind(R.id.categoryTextView) TextView mCategoryTextView;
        @Bind(R.id.ratingTextView) TextView mRatingTextView;

        private Context mContext;

        public RestaurantViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            mContext = itemView.getContext();
        }

        public void bindRestaurant(Restaurant restaurant) {
            mNameTextView.setText(restaurant.getName());
            mCategoryTextView.setText(restaurant.getCategories().get(0));
            mRatingTextView.setText("Rating: " + restaurant.getRating() + "/5");
        }
    }
}

Now that we have the necessary ViewHolder, we can add the remaining three methods required by the RecyclerView.Adapter: onCreateViewHolder(), onBindViewHolder(), and getItemCount():

adapters/RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    private ArrayList<Restaurant> mRestaurants = new ArrayList<>();
    private Context mContext;

    public RestaurantListAdapter(Context context, ArrayList<Restaurant> restaurants) {
        mContext = context;
        mRestaurants = restaurants;
    }

    @Override
    public RestaurantListAdapter.RestaurantViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.restaurant_list_item, parent, false);
        RestaurantViewHolder viewHolder = new RestaurantViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RestaurantListAdapter.RestaurantViewHolder holder, int position) {
        holder.bindRestaurant(mRestaurants.get(position));
    }

    @Override
    public int getItemCount() {
        return mRestaurants.size();
    }

    public class RestaurantViewHolder extends RecyclerView.ViewHolder {
        @Bind(R.id.restaurantImageView) ImageView mRestaurantImageView;
        @Bind(R.id.restaurantNameTextView) TextView mNameTextView;
        @Bind(R.id.categoryTextView) TextView mCategoryTextView;
        @Bind(R.id.ratingTextView) TextView mRatingTextView;
        private Context mContext;

        public RestaurantViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            mContext = itemView.getContext();
        }

        public void bindRestaurant(Restaurant restaurant) {
            mNameTextView.setText(restaurant.getName());
            mCategoryTextView.setText(restaurant.getCategories().get(0));
            mRatingTextView.setText("Rating: " + restaurant.getRating() + "/5");
        }
    }
}
  • The .onCreateViewHolder() method inflates the layout, and creates the ViewHolder object required from the adapter. We will revisit this momentarily.

  • .onBindViewHolder() updates the contents of the ItemView to reflect the restaurant in the given position.

  • .getItemCount() sets the number of items the adapter will display.

  • Finally, we set up our ViewHolder. We find the views and set their values for the item in the list.

Using Custom Adapters with RecyclerView

Now, we are ready to use our RestaurantListAdapter in our RestaurantsActivity. Similar to the way we previously used ListViews in conjunction with ArrayAdapters, we'll call the .setAdapter() method on our new RecyclerView to set RestaurantListAdapter as its new adapter.

Additionally, we'll need to create and set an instance of the LayoutManager the RecyclerView requires. We'll use the LinearLayoutManager.

First, we'll replace these two lines of code near the top of the file:

...
    @Bind(R.id.locationTextView) TextView mLocationTextView;
    @Bind(R.id.listView) ListView mListView;
...

..with these two lines:

...
    @Bind(R.id.recyclerView) RecyclerView mRecyclerView;
    private RestaurantListAdapter mAdapter;
...

Because we're now depending on our RecyclerView and corresponding adapter to display information in our UI, instead of our old LocationText and List views.

We can also remove the following line completely:

...
        mLocationTextView.setText("Here are all the restaurants near: " + location);
...

Because we're not longer using the mLocationTextView it refers to.

Next, we'll add code to instantiate the adapter, associate it with our RecyclerVIew, and assign a layout manager to our overriden run() method in the onResponse() callback of getRestaurants():

RestaurantsActivity.java
...
                    @Override
                    public void run() {
                        mAdapter = new RestaurantListAdapter(getApplicationContext(), restaurants);
                        mRecyclerView.setAdapter(mAdapter);
                        RecyclerView.LayoutManager layoutManager =
                                new LinearLayoutManager(RestaurantsActivity.this);
                        mRecyclerView.setLayoutManager(layoutManager);
                        mRecyclerView.setHasFixedSize(true);
                    }
...

The line mRecyclerView.setHasFixedSize(true); informs mRecyclerView that its width and height should always remain the same. Otherwise, as individual list item views are continually recycled, it may attempt to reset its own size to best fit the content.

If each list item was a different size, the RecyclerView might need to resize as we scrolled to best fit content, but our list items are pretty uniform. So, we can avoid wasting precious processing power by setting a fixed size.

The entire updated file should now look like this:

RestaurantsActivity.java
public class RestaurantsActivity extends AppCompatActivity {
    public static final String TAG = RestaurantsActivity.class.getSimpleName();

    @Bind(R.id.recyclerView) RecyclerView mRecyclerView;
    private RestaurantListAdapter mAdapter;

    public ArrayList<Restaurant> restaurants = 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);
    }

    private void getRestaurants(String location) {
        final YelpService yelpService = new YelpService();
        yelpService.findRestaurants(location, new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) {
                restaurants = yelpService.processResults(response);

                RestaurantsActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mAdapter = new RestaurantListAdapter(getApplicationContext(), restaurants);
                        mRecyclerView.setAdapter(mAdapter);
                        RecyclerView.LayoutManager layoutManager =
                                new LinearLayoutManager(RestaurantsActivity.this);
                        mRecyclerView.setLayoutManager(layoutManager);
                        mRecyclerView.setHasFixedSize(true);
                    }
                });
            }
        });
    }
}

We now have a customized list of restaurants using a RecyclerView. Nice work!

Video Version of this Lesson

Here is the slightly outdated, optional video for this lesson if you would like to review it for reference.

Addressing Version Incompatibility

Sometimes, depending on the specific dependencies of your project, students may receive a build errors at this point in the curriculum. This is due to a version incompatibiity issue with the RecyclerView dependency library. If you experience this issue, attempt the following:

In your build.gradle file, change your compileSdkVersion value to read 25, like so:

build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "23.0.3"
    useLibrary 'org.apache.http.legacy'
...

Then, update the version numbers of any Android dependencies to 25.3.1. (If you've been following along with the curriculum exactly, the only other Android dependencies your project should have are com.android.support:recyclerview-v7 and com.android.support:appcompat). The updated dependencies section of your build.gradle should look like this:

build.gradle
....
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile 'org.robolectric:shadows-support-v4:3.0'
    compile 'com.jakewharton:butterknife:7.0.1'
    compile 'com.squareup.okhttp3:okhttp:3.2.0'
    compile 'com.android.support:recyclerview-v7:25.3.1'
...

At this point, make sure to sync Gradle. If you still experience issues, try cleaning and rebuilding your project.


Example GitHub Repo for MyRestaurants

Terminology


  • RecyclerView`: A newer Android ViewGroup object meant to render any adapter-based View. It's similar to a ListView, but with many updated features, including the ability to implement both horizontal and vertical lists.

    • LayoutManager: Responsible for positioning individual item views inside the RecyclerView. There are three built-in LayoutManager options:
    • LinearLayoutManager: Displays items in a vertical or horizontal scrolling list.
    • GridLayoutManager: Displays items in a grid.
    • StaggeredGridLayoutManager: Displays items in a more staggered grid.
  • RecyclerView.Adapter: Similar to the built-in Android ArrayAdapter. It will populate the data into the RecyclerView. It also converts a Java object into an individual list item View to be inserted and displayed to the user.

  • ViewHolder: An object that stores multiple Views inside the tag field of the Layout so they can be immediately loaded, and you don't have to find them by id repeatedly. A ViewHolder is required by the RecyclerView.Adapter.

  • Inner-class: Also sometimes referred to as a nested class. A class that resides within another class. They have all functionality of a non-nested class, but with limited scope. They also have full access to the class in which they are nested.

Examples


Creating a custom RecyclerView.Adapter:

adapters/RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    private ArrayList<Restaurant> mRestaurants = new ArrayList<>();
    private Context mContext;

    public RestaurantListAdapter(Context context, ArrayList<Restaurant> restaurants) {
        mContext = context;
        mRestaurants = restaurants;
    }

    @Override
    public RestaurantListAdapter.RestaurantViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.restaurant_list_item, parent, false);
        RestaurantViewHolder viewHolder = new RestaurantViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RestaurantListAdapter.RestaurantViewHolder holder, int position) {
        holder.bindRestaurant(mRestaurants.get(position));
    }

    @Override
    public int getItemCount() {
        return mRestaurants.size();
    }

    public class RestaurantViewHolder extends RecyclerView.ViewHolder {
        @Bind(R.id.restaurantImageView) ImageView mRestaurantImageView;
        @Bind(R.id.restaurantNameTextView) TextView mNameTextView;
        @Bind(R.id.categoryTextView) TextView mCategoryTextView;
        @Bind(R.id.ratingTextView) TextView mRatingTextView;
        private Context mContext;


        public RestaurantViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            mContext = itemView.getContext();
        }

        public void bindRestaurant(Restaurant restaurant) {
            mNameTextView.setText(restaurant.getName());
            mCategoryTextView.setText(restaurant.getCategories().get(0));
            mRatingTextView.setText("Rating: " + restaurant.getRating() + "/5");
        }
    }
}

Instantiating and setting RecyclerView.Adapter:

RestaurantsActivity.java
public class RestaurantsActivity extends AppCompatActivity {
    public static final String TAG = RestaurantsActivity.class.getSimpleName();

    @Bind(R.id.recyclerView)
    RecyclerView mRecyclerView;
    private RestaurantListAdapter mAdapter;

    public ArrayList<Restaurant> restaurants = 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);
    }

    private void getRestaurants(String location) {
        final YelpService yelpService = new YelpService();
        yelpService.findRestaurants(location, new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) {
                restaurants = yelpService.processResults(response);

                RestaurantsActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mAdapter = new RestaurantListAdapter(getApplicationContext(), restaurants);
                        mRecyclerView.setAdapter(mAdapter);
                        RecyclerView.LayoutManager layoutManager =
                                new LinearLayoutManager(RestaurantsActivity.this);
                        mRecyclerView.setLayoutManager(layoutManager);
                        mRecyclerView.setHasFixedSize(true);
                    }
                });
            }
        });
    }
}