Recently Started with Android Using RecyclerView

Including Some Aha Moments

How It Began

I do programming for many years, but mainly C and Python. Recently I had the chance to create an Android prototype for which I reused parts of an existing app using RecyclerView. With almost no Java and Android experience this turned out to be quite a challenge. I had some struggle understanding how it work in order to turn the very simple examples to a more complex use-case. Therefore I wrote this tutorial that starts with the simple part and shows an example how to extend and highlight some worth to mention things of RecyclerView. For some deep dive into the RecyclerView I suggest the article by Naveen Goyal I linked at the bottom.

RecyclerView - What's It About

When you read about RecyclerView you find that it is supposed to be more efficient, is flexible and has some build-in animations that help your app to look more professional. As the name suggests a View is something that can be displayed on the screen. That does not mean that it actually is displayed. As the name RecyclerView suggests it is a View itself. So it is a container View that handles displaying other Views. What's special about it, that it is optimized to display views that share a common layout, but have different data to be filled with. E.g. this can be an image with a frame around or card-like boxes that display address data. In the following I will talk of data-set when I mean the subset of data that is used for a specific view. (e.g. the view could be a colored box with some image inside and the data-set the color of the frame and the actual image to be displayed) The combination of view and it's variable content is called item. Note: Though it is one of the strengths of RecyclerView, to scroll-display things that have a similar layout but different content, It's not mandatory to use the exact same layout for the views. It is possible to use completely different ones as well to adapt and extend similar layouts individually. In other words "data" is not limited to numbers and strings but can also be layout changes or even new views within the view.

Set It Up

ViewAdapter

To get started we need our own extension of RecyclerView.ViewAdapter. This is something like the root manager of your views and data-sets.

Here's an empty template to start with:

import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

public class myViewAdapterExample extends RecyclerView.Adapter {
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }
}

RecyclerView has some basic requirements to your data. The first and very important one is that you'r scroll-able list of items will be enumerated consecutively starting with 0. This is the position parameter you will be given from RecyclerView when it calls your onBindViewHolder. So it's your part to tell RecyclerView how many items there are overall and in case you want to remove some or add additional ones. (e.g. you have a database of 100 addresses, then return 100 in getItemCount() .)

    @Override
    public int getItemCount() {
        return 100;
    }

ViewHolder

When your RecyclerView starts to display data it will start picking up items from you until the screen and somewhat beyond is filled. It will do so starting with position 0. But before the onBindViewHolder is called it will ask you to supply a ViewHolder. There is not much you need to add to the ViewHolder supplied by RecyclerView.ViewHolder class but supply your View to it.

If you have the same layout for all items to display it's fairly simple, then there is only one type of ViewHolder you need to supply. Even with different types of ViewHolderYou can do all implementation in your main adapter. Since this can get quite extensive if you deal with different layouts, I prefer to create separate ones:

import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.octopus.recyclerviewexample.R;

public class MyBasicViewHolder extends RecyclerView.ViewHolder {
    public TextView mTitle;
    public MyBasicViewHolder(@NonNull ViewGroup parent) {
        //super will set the result from inflater to itemView of the super class
        super(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_card_layout, parent, false));
        mTitle = itemView.findViewById(R.id.card_title);
    }
}

I seems good practice that those sub-views who's data we want to change are assigned to a variable of that view, like done here with mTitle.

To get the example running we will create two simple layout files and add it to the projects /res/layout folder. The first containing the RecyclerView and second the layout that will be used repeated times for each item. activity_cart.xml, that only contains the RecyclerView

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

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_card_activity"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

my_card_layout.xml, having only a single text line.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">

    <TextView
        android:id="@+id/card_title"
        tools:text="Title"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content" />
</LinearLayout>

Before we can use our Adapter we need to extend the onCreateViewHolder

    public MyBasicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new MyBasicViewHolder(parent);
    }

By default viewType is always 0 and which is fine at this point, since we only have a single layout we want to display. Later on I will show how to extend this to support different ones.

To get the machine running and actually display something some preparation calls are needed from your Activity

import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.octopus.recyclerviewexample.R;
...
//in the onCreate of the activity
        //set the layout that contains the RecyclerView
        setContentView(R.layout.activity_card);
        // set up the RecyclerView, rv_card_activity is the name we gave it in the activity_card.xml
        RecyclerView recyclerView = findViewById(R.id.rv_card_activity);
        //The Layout Manager determines how the items will be displayed
        //There are predefined ones available, e.g. the LinearLayoutManager
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(new MyViewAdapterExample());

If you run this now (and you didn't forget to let getItemCount() return a value x > 0) this will create a scroll-able list of items, that all state 'Title'. This quite a big effort to display a simple list but once it is set up properly it performs well also for huge data sets. But before we continue let's see what RecyclerView does. When the activity is started RecyclerView asks our adpater how many items there are. So we tell him e.g. 100. Then it will start requesting the ViewHolders for the first item by calling onCreateViewHolder and right afterwards onBindViewHolder(RecyclerView.ViewHolder holder, int position) to get it's data updated (which we don't since it's still empty in our example). With this call it supplies us the position so we can determine which of the 100 items we told him that we have, it is talking about. The ViewHolder we get is just the one we returned on onCreateViewHolder. Then it will do the same for the next View that is on position 2. So what's this fuss about, since so far we could have done this without a fancy RecyclerView thing.

The Recycler Magic starts to happen once the visible screen of the phone is filled with our item. Then it will stop requesting further ViewHolders until we start scrolling down the list. Once we do this it might request some more, but it might as well reuse some we supplied previously. (Remember that by default RecyclerView assumes that all ViewHolders are of the same type, so the ViewHolders are exchangable and only differ in the data that was given to them in the onBindViewHolder). Reuse means that a ViewHolder Object we created earlier and that is not visible on the screen anymore is given to us as holder in a onBindViewHoldercall, but for another position, aka another data-set.

Bind to Adapter

Before extending this with different views let's fill in some data:

public class myViewAdapterExample extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new MyBasicViewHolder(parent);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        MyBasicViewHolder mbvHolder = (MyBasicViewHolder) holder;
        mbvHolder.mTitle.setText(Integer.toString(position));
        mbvHolder.itemView.setVisibility(View.VISIBLE);
    }

    @Override
    public int getItemCount() {
        return 100;
    }
}

What we got so far: A list of 100 items that is dynamically filled with data and only uses only about as many ViewHolders as there is needed to display the data on the screen.

Different ViewTypes

Not exiting enough yet? Ok here it is: different ViewTypes!

Before I mentioned that RecyclerView by default assumes a single ViewType. Therefore if we have different ones, we need to tell it. This is done by overriding int getItemViewType(int position). RecyclerView will call this before it requests or binds a ViewHolder from us and what it does is, ask us "Hey what viewType will we use for position x?". You can choose any int values you like to identify your different view-types. RecyclerView will then ask you to supply a new ViewHolder of this type or, in case it has one already that is available for Recycling supplies this directly to the onBindViewHolder. You can even create different types for each position, though it will force RecylerView to only reuse ViewHolders for a particular position and therefore might be a performance issue at some point.

To show how another layout can be used, I created this my_other_card_layout.xml and MyOtherViewHolder.

<LinearLayout 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:orientation="vertical">

    <TextView
        android:id="@+id/card_title"
        tools:text="Title"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content" />
    <TextView
        android:id="@+id/card_content"
        tools:text="Additional Content"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content" />

</LinearLayout>
public class MyOtherViewHolder extends RecyclerView.ViewHolder {
    TextView mContent;
    TextView mTitle;

    public MyOtherViewHolder(@NonNull ViewGroup parent) {
        //super will set the result from inflater to itemView of the super class
        super(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_other_card_layout, parent, false));
        mTitle = itemView.findViewById(R.id.card_title);
        mContent = itemView.findViewById(R.id.card_content);
    }
}

MultiView Adapter Accomplished

import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;


public class myViewAdapterExample extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    //to avoid juggling with magic int nor upgly @Intdef constructions I
    //use java enum as a set of consecutively numbered integers
    enum ViewTypeEnum {BASIC_VIEW, OTHER_VIEW}
    static final ViewTypeEnum[] ordinalToViewTypeEnum = ViewTypeEnum.values();


    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ViewTypeEnum viewTypeEnum = ordinalToViewTypeEnum[viewType];
        RecyclerView.ViewHolder viewHolder;
        switch(viewTypeEnum){
            case BASIC_VIEW:
                viewHolder = new MyBasicViewHolder(parent);
                break;
            case OTHER_VIEW:
                viewHolder = new MyOtherViewHolder(parent);
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + viewTypeEnum);
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        ViewTypeEnum viewTypeEnum = ordinalToViewTypeEnum[getItemViewType(position)];
        switch(viewTypeEnum){
            case BASIC_VIEW:
                //fill basic view with data of it's position
                MyBasicViewHolder mbvHolder = (MyBasicViewHolder) holder;
                mbvHolder.mTitle.setText(Integer.toString(position));
                break;
            case OTHER_VIEW:
                //fill other view with data of it's position
                MyOtherViewHolder movHolder = (MyOtherViewHolder) holder;
                movHolder.mTitle.setText(Integer.toString(position));
                movHolder.mContent.setText(Integer.toString(position));
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + viewTypeEnum);
        }
        holder.itemView.setVisibility(View.VISIBLE);
    }

    @Override
    public int getItemCount() {
        return 100;
    }

    @Override
    public int getItemViewType(int position){
        if(position%2 == 0) {
            return ViewTypeEnum.BASIC_VIEW.ordinal();
        }
        return ViewTypeEnum.OTHER_VIEW.ordinal();
    }
}

My Ahas

RecyclerView might request a new ViewHolders for a position any time, even when the item is still visible on the screen. Consequently there might actually be two (or more?) ViewHolder instances around within RecyclerView for the exact same item at the exact same position. In the Deep Dive Link below there is some background why this occurs. Don't keep instances of them by yourself. Don't do anything in the bind that assumes there is only a single instance of ViewHolder around for this position.

Anything can be done onBindViewHolder except creating the ViewHolder instance itself. There is no hard restriction that only changing data is handled in onBindViewHolder call. Anything can as well be done on onBindViewHolder, but it typically will be called much more often than create and therefore might impact your apps performance.

If you are looking for some more tutorials on RecyclerView I suggest you check out these:

RecyclerView Tutorial With Example In Android Studio Deep Dive Into the RecyclerView. Introduction | by Naveen Goyal