RecyclerView - Creating Dynamic Lists and Grids in Android - Part 1

Getting started with RecyclerView

The average number of new apps that are submitted to Google playstore are north of 500 apps. However, an average user downloads no more than 60-70 apps onto his mobile phone. With competition heating up for user mindhsare, customer experience is as important as the functionality of the app. With the mobile form factor one of the important aspects of customer experience is how you handle your content and if that content is easy to interact with for your app users.

Content Display is the King of Experience
In android applications that are data intensive, such data is displayed either in the form of list or grids. While most of the developers still prefer ListView for the same, Google recommends the use of RecyclerView for optimized applications. Many developers hesitate to implement RecyclerView in their application because of the complexities involved in its implementation. However following few basic guidelines and implementation rules, RecyclerView can turn out to be a breeze.

In this blog I will be writing step by step procedure of making an app similar to Whatsapp in appearance with RecyclerView that responds to user actions to perform specific UI changes. I will also discuss about the common mistakes done by the developers which put them into thinking and debugging which kills their precious development time.

You can also import the whole project from GitHub repository

Demo App
Introduction to RecyclerView

The RecyclerView widget is a more advanced and flexible version of ListView. This widget is a container for displaying large data sets that can be scrolled very efficiently by maintaining a limited number of views. Use the RecyclerView widget when you have data collections whose elements change at runtime based on user action or network events.

Introduction to RecylerView

picture credits - Google developers

Difference between RecyclerView and ListView

ListView is ideally used to display linear data items in vertically scrollable list that contains one data set in each row. But RecyclerView with its highly customizable behavior and LayoutManagers allows users to display the data set in vertical or horizontal scrollable views in the form of Linear Lists or Grids.

Another major difference between the RecyclerView and ListView is that RecyclerView optimizes the performance of the application by Recycling the views to display the row items, that is, it uses same view to display multiple rows unlike the ListView that creates a new View everytime a row is visible. This increases the overhead of view lookup through findViewById() which is called for every row. Though ListView can be optimized with the use of ViewHolders, but it involves a complicated implementation in getView() method of the ListView Adapter.

Setting up the project
Before we can begin working with RecyclerView we need to add few dependencies to the Gradle file at module level.

dependencies {
    //other dependencies
    compile 'com.android.support:appcompat-v7:24.1.1'
    compile 'com.android.support:recyclerview-v7:24.1.1'
    compile 'com.android.support:cardview-v7:24.1.1'
}
Make sure to add all the required dependencies for the project to prevent long debugging later.

Once your project is synced with the new gradle changes, you can proceed by setting up the XML Layout to display the RecyclerView.

activity_recycler_view.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"
    tools:context=".activities.RecyclerViewActivity">
    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="@dimen/list_top_padding"
        android:id="@+id/recycler_view"/>
</RelativeLayout>
Its better to remove any padding from the parent layout as it allows to customize individual list item.
Adding and displaying the items in RecyclerView

RecyclerView can be declared and used in Activity or Fragment based on the project. In this project I will implement ReyclerView in Activity.


public class RecyclerViewActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private CustomAdapter mAdapter;
    private List<RecyclerViewClass> mItems;
    private RecyclerView.LayoutManager mLayoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_view);
        mRecyclerView = (RecyclerView)findViewById(R.id.recycler_view);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);
        // use a linear layout manager
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mRecyclerView.addItemDecoration(new RecyclerViewDecorator(this));
        Bitmap bitmap = getRoundedShape(BitmapFactory.decodeResource(getResources(),R.drawable.ironman));
        Drawable d = new BitmapDrawable(getResources(), bitmap);
        mItems = new ArrayList<>();
        //adding test items to the list
        for(int i=0; i<30; i++){
            mItems.add(i, new RecyclerViewClass(i+" string1", i+" string2", d));

        }
        mAdapter = new CustomAdapter(this, mItems);
        mRecyclerView.setAdapter(mAdapter);
    }
}
LayoutManager and Item Animator

A LayoutManager is responsible for measuring and positioning item views within a RecyclerView as well as determining the policy for when to recycle item views that are no longer visible to the user. By changing the LayoutManager a RecyclerView can be used to implement a standard vertically scrolling list, a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock layout managers are provided for general use.

In this example I will demonstrate the use of LinearLayoutManager which is used to display the vertically scrolling list.


RecyclerView.LayoutManager mLayoutManager;
mLayoutManager = new LinearLayoutManager(this);    

Item animator is used to animate the list operations like addition and removal of items from the list, scrolling the list, etc.

In this example I have used DefaultItemAnimator() which provides smooth scrolling effect for addition and removal of items.


mRecyclerView.setItemAnimator(new DefaultItemAnimator());
Setting up the RecyclerView.Adapter

It is responsible for binding the data corresponding to a ViewHolder and displaying it in the RecyclerView. Most of the operations related to the list like responding to the click, changing the UI of the row, etc., are done in the adapter. It is recommended to pass the data and the context of the calling class while calling the constructor, as it can be used later on in the project for various purposes like initializing the Interface variable or accessing the application resources.

Adaper Class for RecyclerView


public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.CustomRecyclerViewHolder>{

    List<RecyclerViewClass> mItems;
    Context mContext;
    boolean onLongPressReceived = false;

    public CustomAdapter(Context context, List<RecyclerViewClass> items){
        mContext = context;
        mItems = items;
    }

    @Override
    public CustomRecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // create a new view
        View v = LayoutInflater.from(mContext)
                .inflate(R.layout.custom_recyclerview_layout, parent, false);
        //set the margin if any, will be discussed in next blog
        return new CustomRecyclerViewHolder(v);
    }

    @Override
    public void onBindViewHolder(final CustomRecyclerViewHolder holder, int position) {
        holder.mAvatarView.setImageDrawable(mItems.get(position).getmImage_url());
        holder.mMsg1.setText(mItems.get(position).getMessage1());
        holder.mMsg2.setText(mItems.get(position).getMessage2());
        holder.cardView.setBackgroundColor(ContextCompat.getColor(mContext, R.color.list_unselected));
        holder.mDeleteRow.setVisibility(View.INVISIBLE);
        if (mItems.get(position).getmIsChecked()) {
            holder.checkboxHolder.setVisibility(View.VISIBLE);
            holder.mCheckBox.setChecked(true);
        } else {
            holder.checkboxHolder.setVisibility(View.GONE);
            holder.mCheckBox.setChecked(false);
        }
    }
    @Override
    public int getItemCount() {
        return mItems.size();
    }
}

Adapter class explained

Constructor is called while setting up the adapter from the main class. All the necessary variables are initialized including the interface variable if calling class implements that interface.

Then a call is made to onCreateViewHolder() method where we inflate our custom view and pass it to the holder. This view is recycled through out the RecyclerView.

As the list items are viewed on the screen, corresponding list items makes call to onBindViewHolder() method which binds the view to the row.

Common mistake that developers do is use the position passed to onBindViewHolder() method for long term operations.
One good feature of RecyclerView is that it do not create a new View for all the rows every time to optimize the performance of the application. Hence if we try to use the position passed as parameter to the method, we might end up putting the details of one View into the other which invokes the UI blunder. Hence it is recommended to use holder.getAdapterPosition() method to get the position of the current visible view in the adapter. This call to get the position is to be used in Listeners to get the position of the visible View in the Adapter and make changes to it. More details about position of view in adapter and actual position is given in Common Mistakes section.

Setting up RecyclerView.ViewHolder

RecyclerView.ViewHolder holds the data of the views contained in the custom view layout file. It is initialized when the adapter is attached to the RecyclerView and onCreateViewHolder() method is called. Based on the use, View.OnClickListeners or other listeners can be implemented in the RecyclerView.ViewHolder class.

In general RecyclerView.ViewHolder class is inner class of RecyclerView.Adapter class.


public class CustomRecyclerViewHolder extends RecyclerView.ViewHolder{

        private TextView mMsg1, mMsg2;
        private ImageView mAvatarView;
        private CheckBox mCheckBox;
        private LinearLayout checkboxHolder;
        private ImageView mDeleteRow;
        private CardView cardView;
        public CustomRecyclerViewHolder(View itemView) {
            super(itemView);
            mMsg1 = (TextView)itemView.findViewById(R.id.text_view1);
            mMsg2 = (TextView)itemView.findViewById(R.id.text_view2);
            mAvatarView = (ImageView)itemView.findViewById(R.id.avatar_holder);
            mCheckBox = (CheckBox)itemView.findViewById(R.id.checkbox);
            checkboxHolder = (LinearLayout)itemView.findViewById(R.id.checkbox_holder);
            mDeleteRow = (ImageView)itemView.findViewById(R.id.delete_row);
            cardView = (CardView)itemView.findViewById(R.id.card_holder);
        }
    }    
Creating a custom view to display list items

This XML file is used to create a view inflated in ViewHolder which is used to display each row or item of the list. In this blog I will discuss about creating the custom view for linear list, views for grid layout will be discussed in next blog.

custom_recyclerview_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_holder"
    android:layout_width="match_parent"
    android:layout_height="@dimen/list_top_margin"
    app:cardCornerRadius="0dp">
    <RelativeLayout
        android:id="@+id/card_linear_holder"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ImageView
            android:layout_marginLeft="@dimen/activity_horizontal_margin"
            android:layout_marginRight="@dimen/activity_horizontal_margin"
            android:layout_width="@dimen/profile_pic_size"
            android:layout_height="@dimen/profile_pic_size"
            android:id="@+id/avatar_holder"
            android:layout_centerVertical="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true" />
        <LinearLayout
            android:id="@+id/text_holder_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_toRightOf="@+id/avatar_holder_layout"
            android:layout_toEndOf="@+id/avatar_holder_layout"
            android:layout_toLeftOf="@+id/checkbox_holder"
            android:layout_toStartOf="@+id/checkbox_holder"
            android:paddingLeft="0dp"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingStart="0dp"
            android:paddingEnd="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
            android:paddingBottom="@dimen/activity_vertical_margin">
            <TextView
                android:id="@+id/text_view1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="@dimen/list_item_1"
                android:textColor="@color/black"/>
            <TextView
                android:id="@+id/text_view2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="@dimen/list_item_2" />
        </LinearLayout>
        <LinearLayout
            android:id="@+id/checkbox_holder"
            android:visibility="gone"
            android:layout_width="@dimen/check_box_holder_width"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"
            android:gravity="center">
            <CheckBox
                android:id="@+id/checkbox"
                android:layout_width="@dimen/check_box_width"
                android:layout_height="@dimen/check_box_width"/>
            <ImageView
                android:id="@+id/delete_row"
                android:visibility="invisible"
                android:layout_width="@dimen/check_box_width"
                android:layout_height="@dimen/check_box_width"
                android:src="@android:drawable/ic_menu_close_clear_cancel"/>
        </LinearLayout>
    </RelativeLayout>
</android.support.v7.widget.CardView>
Add all the required attributes in the layout files to have support for multiple screens and versions of android. For example android:layout_toStartOf attribute is introduced in API 17 to support better symmetric layouts, but to support versions below API 17 android:layout_toRightOf must also be added.

This method is used to generate circular bitmaps to show as Avatars in ImageView

Using the dimensions from dimens.xml helps to resize the resource based on the density of the screen. No special classes to convert dp to pixels will be required.

public Bitmap getRoundedShape(Bitmap scaleBitmapImage) {
        int targetWidth = (int)getResources().getDimension(R.dimen.profile_pic_size);
        int targetHeight = (int)getResources().getDimension(R.dimen.profile_pic_size);;
        Bitmap targetBitmap = Bitmap.createBitmap(targetWidth,
                targetHeight,Bitmap.Config.ARGB_8888);

        Canvas canvas = new Canvas(targetBitmap);
        Path path = new Path();
        path.addCircle(((float) targetWidth - 1) / 2,
                ((float) targetHeight - 1) / 2,
                (Math.min(((float) targetWidth),
                        ((float) targetHeight)) / 2),
                Path.Direction.CCW);

        canvas.clipPath(path);
        canvas.drawBitmap(scaleBitmapImage,
                new Rect(0, 0, scaleBitmapImage.getWidth(),
                        scaleBitmapImage.getHeight()),
                new Rect(0, 0, targetWidth, targetHeight), null);
        return targetBitmap;
    }        

Setting up POJO class for list items

POJO stands for PLAIN OLD JAVA OBJECT. POJO class helps to interact with the Adapter and hold the data corresponding to each row or grid in the view. It contains a collection of setters and getters which are responsible for manipulating the data contained in the view. Without a proper POJO class, your app can misbehave or do not respond to the changes as expected. POJO class contains list of all variables and corresponding setters and getters that are necessary to display the data appropriately in the list.

RecyclerViewClass.java


public class RecyclerViewClass {
    private String mMsg1, mMsg2;
    private Drawable mImage_url;
    private boolean mIsChecked;

    public RecyclerViewClass(String mMsg1, String mMsg2, Drawable mImage_url){
        this(mMsg1, mMsg2, mImage_url, false);
    }

    public RecyclerViewClass(String mMsg1, String mMsg2, Drawable mImage_url, boolean mIsChecked){
        this.mMsg1 = mMsg1;
        this.mMsg2 = mMsg2;
        this.mImage_url = mImage_url;
        this.mIsChecked = mIsChecked;
    }

    //setters
    public void setMessage1(String mMsg1){
        this.mMsg1 = mMsg1;
    }
    public void setMessage2(String mMsg2){
        this.mMsg2 = mMsg2;
    }
    public void setmImage_url(Drawable mImage_url){
        this.mImage_url = mImage_url;
    }
    public void setmIsChecked(boolean mIsChecked){
        this.mIsChecked = mIsChecked;
    }

    //getters
    public String getMessage1(){
        return mMsg1;
    }
    public String getMessage2(){
        return mMsg2;
    }
    public Drawable getmImage_url(){
        return mImage_url;
    }
    public boolean getmIsChecked(){
        return mIsChecked;
    }
}
It is recommended to use setters and getters for each and every variable present in this class
Responding to user actions using POJO class

Applications consisting of RecyclerView consists of several user actions like scrolling, selecting a list item, adding and removing the list items, etc. Hence it becomes important to handle all those actions in appropriate way to prevent any unexpected behavior of the application.

Handling the backPress

We override the onBackPressed() method of Activity in which RecyclerView is hosted to control the navigation of the application. That is if user long clicked the list item to select, dis select or delete the row item, then back press should take us back to normal state instead of closing the application.


    @Override
    public void onBackPressed() {
        if(mAdapter.getLongPressStatus()){
            mAdapter.setOnLongPressStatus(false);
        }else{
            super.onBackPressed();
        }
    }        

Responding to clicks and other user events

An app is incomplete unless it responds to user actions like long press, clicking on a particular view or checking or unchecking the check box.

There are several ways to add listeners, few of them are listed and explained below:

  • Implementing the listener interface in ViewHolder class
    • This is generally done when user wants to get the details of the View based on the clicks performed. In general this should be avoided if any manipulations to the view is to be done.
  • Implementing the listener interface in Adapter class
    • This is generally done when making common changes to rows like changing background of all rows, that is, an event which is independent of the row position.
  • Adding listeners to the variable directly.
    • This is done when any action specific to any view is to be performed. And these listeners are generally added to the variables in onBindViewHolder()

In this example I will be explaining the use of listeners in Independent variables in onBindViewHolder() method.

Responding to long click on the list item.

holder.cardView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                onLongPressReceived = true;
                mItems.get(holder.getAdapterPosition()).setmIsChecked(true);
                notifyDataSetChanged();
                return true;
            }
        });
Responding to long press

Responding to check-box change

holder.mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {                updateMainClass.updateListBackground(holder.getAdapterPosition(), b);
            }
        });
Responding to check and uncheck

Responding to delete row

holder.mDeleteRow.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {                updateMainClass.updateItemList(holder.getAdapterPosition());
            }
        });
Deleting the row

Adding setters and getters to access adapter variables

Though adapter variables can be accessed directly from the Activity but it is good practice to use the setters and getters to change the value of any variable in adapter to prevent any misuse of variable such as changing its value in one method and accessing it another method without checking the conditions that affects its value.

public void setOnLongPressStatus(boolean status){
        onLongPressReceived = status;
        notifyDataSetChanged();
    }    
public boolean getLongPressStatus(){
        return onLongPressReceived;
    }

Communicating with the Activity from Adapter class

public class RecyclerViewActivity extends AppCompatActivity implements CustomAdapter.UpdateMainClass {

    @Override
    public void updateItemList(int position) {
        mItems.remove(position);
        mAdapter.notifyItemRemoved(position);
    }
    @Override
    public void updateListBackground(int position, boolean isChecked) {
        try {            mItems.get(position).setmIsChecked(isChecked);
            mAdapter.notifyItemChanged(position);
        }catch(IllegalStateException e){
            //do nothing
        }
    }
}        

Declare and use the interface in your adapter class as given below. Make sure to check whether the Activity is implementing the Interface or not before initializing and using the interface variable.

//creating the interface
public interface UpdateMainClass{
        void updateItemList(int position);
        void updateListBackground(int position, boolean isChecked);
    }
//declaring and initializing the interface variable in Adapter class
UpdateMainClass updateMainClass;
public CustomAdapter(Context context, List<RecyclerViewClass> items){
        //checking whether the calling class implements the interface or not
        if(context instanceof UpdateMainClass){
            updateMainClass = (UpdateMainClass)context;
        }
    }

Setting up RecyclerView.ItemDecoration

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

All ItemDecorations are drawn in the order they were added.

RecyclerViewDecorator.java

public class RecyclerViewDecorator extends RecyclerView.ItemDecoration {
    Context mContext;
    Drawable mDivider;
    public RecyclerViewDecorator(Context mContext){
        this.mContext = mContext;
        mDivider = ContextCompat.getDrawable(mContext, R.drawable.divider_line);
    }
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int dividerLeft = (int)mContext.getResources().getDimension(R.dimen.list_left_margin);
        int dividerRight = parent.getWidth() - (int)mContext.getResources().getDimension(R.dimen.activity_horizontal_margin);
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount - 1; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int dividerTop = child.getBottom() + params.bottomMargin;
            int dividerBottom = dividerTop + mDivider.getIntrinsicHeight();
            mDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
            mDivider.draw(c);
        }
    }
}        
Common mistakes done by developers while implementing RecyclerView

Few common mistakes done by the users while implementing the RecyclerView are

  • Not including all the attributes in the xml files results in unexpected resizing and positioning of the view.
  • Storing the position received from onBindViewHolder() method results in calling the view at given layout position instead of view at adapter position.
  • Not using the POJO class to store the details of the view results in unexpected behavior like clicking one view activating two or more views.
  • Accessing adapter variables directly from the calling class might result in changing the variables that might effect the performance of the app.

Difference between Layout Position and Adapter Position

Layout position is position of an item in the latest layout calculation. This is the position from the LayoutManager's perspective. And Adapter Position is position of an item in the adapter. This is the position from the Adapter's perspective.

That is, once RecyclerView recycles the views, new positions of the ViewHolder is calculated based on its visibility. But Adapter position remains constant unless addition or removal of items are done. If user tries to access the view based on the LayoutPosition, the result may not be as expected, but calling the AdapterPosition method returns the position of the holder in whole adapter.

Preview of part 2

In next blog I will be covering further topics like responding to UI changes like screen rotation using saved instances, adding scrolling animations to the grid, converting list to grid or vice versa, controlling the nested scroll and handling UI changes like addition or removal of views in ViewHolder.

You can also import the whole project from GitHub repository

Mohammed Atif

Explorer...

Hyderabad, India