RecyclerView Part 1: Fundamentals For ListView Experts

Bill Phillips's Headshot
Bill Phillips Android

Time to come clean: I was late to the game on RecyclerView. Way late!

I have only myself to blame. You can tell from its description that RecyclerView is supposed to replace ListView, and there aren’t a lot of Views more important than ListView in the toolbag. Seems pretty clear: RecyclerView is important.

But it’s also a lot different, right? Ugh. So I put it off until a couple of weeks ago, when I was putting together a talk for one of our Hack Nights. I ended up doing a fair amount of research, which was—I admit it—a lot of fun.

It turns out that RecyclerView is really cool, and worth switching to. It makes a lot of previously hard things much, much, much easier. Most importantly, it makes animating items into and out of a list of content doable in an hour or two, instead of a couple of days.

To figure out all this stuff, I decided to swap out the ListView in our Android programming guide’s CriminalIntent exercise to use RecyclerView. I found that most of the work is easy, and the final code satisfyingly clean.

Except for one bit: choice modes. The setChoiceMode() method is gone, and getting a really solid RecyclerView that cleanly transitions into a multiple choice caused some interesting problems. Over the course of a few blog posts, I’ll talk about the path I followed, and share my solutions with you. I’ll start here by talking about the basics of RecyclerView.

RecyclerView Does Less

Let’s talk a bit about the big changes in RecyclerView if you’re trying to replace a ListView. Step zero, of course, is to import the RecyclerView library with this line in your build.gradle:

compile 'com.android.support:recyclerview-v7:+'

Next: there is a reason setChoiceMode(int) is gone, and it’s a good one. RecyclerView can do more than ListView, but the RecyclerView class itself has fewer responsibilities than ListView. Out of the box, RecyclerView does not:

  • position items on the screen
  • animate views
  • handle any touch events apart from scrolling

All of this stuff was baked in to ListView, but RecyclerView uses collaborator classes to do these jobs instead.

For the first two, RecyclerView uses LayoutManager and ItemAnimator. RecyclerView comes with a default ItemAdapter, so you need not worry about that. You do have to give it a LayoutManager to position the items. Here is what our CriminalIntent app’s list fragment onCreateView() looks like with a RecyclerView:

        @Override
    public View onCreateView(LayoutInflater inflater, 
            ViewGroup parent, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_recyclerview, parent, false);

        mRecyclerView = (RecyclerView) v.findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mCrimes = CrimeLab.get(getActivity()).getCrimes();
        mRecyclerView.setAdapter(new CrimeAdapter());

        return v;
    }

LinearLayoutManager lives in the RecyclerView support library. It makes your RecyclerView position its items exactly like ListView did. There are other layout managers to layout in a grid, or a staggered grid. As for CrimeAdapter, I’ll talk about that in a moment.

To tell when an item is clicked on ListView, you would do something like this:

        public View onCreateView(LayoutInflater inflater, ViewGroup parent,
            Bundle savedInstanceState) {
        ...

        mListView.setOnItemClickListener(this);

        ...
    }

    public void onItemClick(AdapterView<?> parent, View view, int position,
            long id) {
        // handle item click
    }

You could also handle click events through listeners on the individual items your adapter returns. This was not common or recommended, though, because ListView gave you some nice things like ListView.setChoiceMode(int), which built selectable items on top of click handling.

However, RecyclerView has mostly shed responsibility for those events, which is why it does not have choice modes. You cannot provide an OnItemClickListener to tell when items are selected. RecyclerView does provide an OnItemTouchListener, but this isn’t the same thing: it does not tell you which item was touched. You can use the MotionEvent to find out which item was touched, but for most situations this is more work than is necessary.

ViewHolder

The other big difference important to know here is the elevated role of the view holder. If you learned how to write ListViews from us, you may not know about the view holder pattern because we don’t teach it. A view holder is an object attached to each row in your ListView. A typical implementation would look like this:

        private static class ViewHolder {
        private CheckBox mSolvedCheckBox;
    }

    private class CrimeAdapter extends ArrayAdapter<Crime> {
        public CrimeAdapter(ArrayList<Crime> crimes) {
            super(getActivity(), 0, crimes);
        }

        @Override
        public View getView(int position, View convertView, 
                ViewGroup parent) {
            ViewHolder holder;
            if (null == convertView) {
                convertView = LayoutInflater.from(getActivity())
                    .inflate(R.layout.list_item_crime, parent, false);
                holder = new ViewHolder();
                holder.mSolvedCheckBox = (CheckBox) convertView
                    .findViewById(R.id.solvedCheckBox);

                convertView.setTag(holder);
            }

            ViewHolder holder = (ViewHolder) convertView.getTag();
            Crime crime = getItem(postion);
            holder.mSolvedCheckBox.setChecked(crime.isSolved());

            return convertView;
        }
    }

When the view is inflated, you also create an instance of this ViewHolder object and populate it with data you are interested in. Then you attach it to the view by calling setTag().

For each view you create, you create a ViewHolder, too. ViewHolder has historically been used to achieve better scrolling performance: it saves the need to call findViewById() every time you need to access solvedCheckBox. If you have other costly operations that you would rather not perform on each call to getView(), you can stash results from those in the ViewHolder as well.

Here is how we would prefer to write the equivalent of the above in a RecyclerView:

        private class CrimeHolder extends ViewHolder {
        private final CheckBox mSolvedCheckBox;
        private Crime mCrime;

        public CrimeHolder(View itemView) {
            super(itemView);

            mSolvedCheckBox = (CheckBox) itemView
                .findViewById(R.id.crime_list_item_solvedCheckBox);
        }

        public void bindCrime(Crime crime) {
            mCrime = crime;
            mSolvedCheckBox.setChecked(crime.isSolved());
        }
    }

    private class CrimeAdapter 
            extends RecyclerView.Adapter<CrimeHolder> {
        @Override
        public CrimeHolder onCreateViewHolder(ViewGroup parent, int pos) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.list_item_crime, parent, false);
            return new CrimeHolder(view);
        }

        @Override
        public void onBindViewHolder(CrimeHolder holder, int pos) {
            Crime crime = mCrimes.get(pos);
            holder.bindCrime(crime);
        }

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

Note that there is no ArrayAdapter, so your Adapter must hook up to a List by hand. This isn’t too hard, thankfully.

Just like in the ListView Adapter example, you have two classes here: an Adapter and a ViewHolder. In a RecyclerView adapter, though, the ViewHolder is more of a keystone piece of the system. Instead of creating and binding Views, your RecyclerView.Adapter creates and binds ViewHolders.

The ViewHolders you create are beefier, too. They subclass RecyclerView.ViewHolder, which has a bunch of methods RecyclerView uses. ViewHolders know which position they are currently bound to, as well as which item ids (if you have those).

In the process, ViewHolder has been knighted. It used to be ListView’s job to hold on to the whole item view, and ViewHolder only held on to little pieces of it. Now, ViewHolder holds on to all of it in the ViewHolder.itemView field, which is assigned in ViewHolder’s constructor for you.

Since ViewHolder has this new responsibility, it starts to make a lot of sense to give it other responsibilities, too. So in the new implementation we’ve added a new method, bindCrime(), which does a lot of the plumbing work that used to go into getView(). ViewHolder also becomes the most natural place to handle any click events for your specific item:

        private class CrimeHolder extends ViewHolder 
            implements View.OnClickListener {
        ...

        public CrimeHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this);

            ...
        }

        @Override
        public void onClick(View v) {
            if (mCrime != null) {
                Intent i = CrimeActivity.getIntent(v.getContext(), mCrime);
                startActivity(i);
            }
        }
    }

In ListView, there was some ambiguity about how to handle click events: Should the individual views handle those events, or should the ListView handle them through OnItemClickListener? In RecyclerView, though, the ViewHolder is in a clear position to act as a row-level controller object that handles those kinds of details.

We saw earlier that LayoutManager handled positioning views, and ItemAnimator handled animating them. ViewHolder is the last piece: it’s responsible for handling any events that occur on a specific item that RecyclerView displays.

Choice Modes and What’s Next

So what about choice modes? Where do they fit into all this? In the way we’ve shown here, into the ViewHolder, naturally enough, because it handles item click events.

This is where I have some bad news: ViewHolder does not give you any tools out of the box to implement selection. To get single or multiselect going, I’ve got two problems that I will address in my next blog post:

  1. Keeping track of selections. There needs to be some code that knows which items are selected, which are not, and whether selection is activated or not.

  2. Showing which items are selected. ListView handled this for you by calling setActivated() on each item’s view. You can do that on Lollipop, too, but there are a few gotchas in implementing it smoothly.

Learn how to tackle choice modes in Lollipop in Part 2 of this series.

Tags: Lollipop

Are you looking for a partner in developing an Android app? Do you want to accelerate your learning? Bill Phillips and the rest of the nerds can teach you the latest and greatest in Android development.

Recent Comments

comments powered by Disqus