Contents
The RecyclerView class, delivered as part of the v7 support library suite, is an improved version of the ListView and the GridView classes provided by the Android framework.
In other words, the RecyclerView is a new ViewGroup that is prepared to render any adapter-based view in a similar way.
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. It requires you to use the ViewHolder pattern which improves performance as you avoid initializing views every time.
One advantage over ListView or GridView is the built in support for animations as items are added, removed, or repositioned. Moreover, respect to ListView, RecyclerView is much more customizable.
Unlike the ListView, the RecyclerView also provides a choice of three built-in layout managers to control the way in which the list items are presented to the user:
LinearLayoutManager - the list items are presented as either a horizontal or vertical scrolling list.GridLayoutManager - the list items are presented in grid format. This manager is best used when the list items of are of uniform size.StaggeredGridLayoutManager - the list items are presented in a staggered grid format. This manager is best used when the list items are not of uniform size.For situations where none of the three built-in managers provide the necessary layout, custom layout managers may be implemented by subclassing the RecyclerView.LayoutManager class.
The implementation of RecyclerView requires a few classes to be implemented. The most important classes are listed in the following table.
| Class | Purpose | Optional |
|---|---|---|
| Adapter | Provides the data and responsible for creating the views for the individual entry. | Required |
| ViewHolder | Contains references for all views that are filled by the data of the entry | Required |
| LayoutManager | Contains references for all views that are filled by the data of the entry | Required, but default implementations available |
| ItemDecoration | Responsible for drawing decorations around or on top of the view container of an entry | Default behavior, but can be overridden |
| ItemAnimator | Responsible to define the animation if entries are added, removed or reordered | Default behavior, but can be overridden |

Comparison between RecyclerView and ListView
There are a lots of new features in RecyclerView that are not present in existing ListView. The RecyclerView is more flexible, powerful and a major enhancement over ListView. Here I will try to give you a detailed insight into it.
ListView can only layout the items in Vertical Arrangement and that arrangement cannot be customized according to our requirements. Suppose we need to create a horizontal list then that thing is not feasible with default ListView. But with introduction of Recyclerview we can easily create a horizontal or vertical List. By using LayoutManager component of RecyclerView we can easily define the orientation of items.ListView adapters do not require the use of ViewHolder but RecyclerView require the use of ViewHolder that is used to store the reference of view’s. In ListView it is recommended to use the ViewHolder but it is not compulsion but in RecyclerView it is mandatory to use ViewHolder which is the main difference between RecyclerView and ListView. ViewHolder is a static inner class in our Adapter which holds references to the relevant view’s. By using these references our code can avoid time consuming findViewById() method to update the widgets with new data.ListView we use many adapter‘s like ArrayAdapter for displaying simple array data, BaseAdapter and SimpleAdapters for custom lists. In RecyclerView we only use RecyclerView.Adapter to set the data in list. ListView are lacking in support of good animation. RecyclerView brings a new dimensions in it. By using RecyclerView.ItemAnimator class we can easily animate the view.ListView dynamically decorating items like adding divider or border was not easy but in RecyclerView by using RecyclerView.ItemDecorator class we have a huge control on it.Our plan is
RecylerView and CardView dependencies to build.gradle (Module:app).RecylerView to our activity_main.xml and complete this layout.RecyclerView using CardView.RecylerViewHolder java class for Recyclerview by extending RecyclerView.ViewHolder.RecylerAdapter java class for Recyclerview by extending RecyclerView.Adapter.RecylerView in MainActivity.1. Open your Android Studio and create a new project
2. Go to GradleScripts > build.gradle (Module:app) and add below libraries to this file under dependencies block. My dependencies block is
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:27.1.0'
compile 'com.android.support:recyclerview-v7:27.1.0'
compile 'com.android.support:cardview-v7:27.1.0'
}
3. Open your activity_main.xml file and add Recyclerview to it
<?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=".MainActivity">
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rcView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:paddingTop="16dp"
android:scrollbars="vertical"
/>
</RelativeLayout>
4. Create a new layout for single row of Recyclerview using CardView.
CardView widget can be used to create simple cards. CardView extends the FrameLayout class and lets you show information inside cards that have a consistent look across the platform. CardView widgets can have shadows and rounded corners.
For single row of RecyclerView I am going to make a simple layout with one ImageView and two text for title and description. The complete layout is wrapped inside CardView to give it some good look. Here is my item_list.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:background="#C5CAE9"
android:foreground="?attr/selectableItemBackground"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<RelativeLayout
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="10dp"
android:scaleType="centerCrop"
android:src="@android:drawable/star_big_on" />
<TextView
android:id="@+id/title"
android:layout_centerVertical="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_toRightOf="@+id/avatar"
android:text="Title"
android:textColor="#000000"
android:textAppearance="?attr/textAppearanceListItem"
android:textSize="16sp" />
<TextView
android:id="@+id/desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginLeft="16dp"
android:layout_toRightOf="@+id/avatar"
android:textColor="#000000"
android:ellipsize="end"
android:singleLine="true"
android:text="Movie from IMDB 250"
android:textAppearance="?attr/textAppearanceListItem"
android:textSize="14sp" />
</RelativeLayout>
</android.support.v7.widget.CardView>
5. Create a RecylerViewHolder java class for RecyclerView by extending RecyclerView.ViewHolder.
RecylerView uses a ViewHolder to store references to the views for one entry in the RecylerView. A ViewHolder class is typically a static inner class in your adapter which holds references to the relevant views. With these references your code can avoid the findViewById() method in an adapter to find the views which should be filled with your new data. This pattern avoids looking up the UI components all the time the system shows a row in the list and this is approximately 15% faster then using the findViewById() method.
Right click on your package and create a new java class name it RecyclerViewHolder and extends RecyclerView.ViewHolder. In this define two TextView and an ImageView of item_list.xml file. Copy and paste below code to your RecyclerViewHolder class. Here is my complete code of RecyclerViewHolder
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
TextView tv1, tv2;
ImageView imageView;
public RecyclerViewHolder(View itemView) {
super(itemView);
tv1 = (TextView) itemView.findViewById(R.id.title);
tv2 = (TextView) itemView.findViewById(R.id.desc);
imageView = (ImageView) itemView.findViewById(R.id.avatar);
}
}
6. Create an RecylerAdapter java class for RecyclerView by extending RecyclerView.Adapter.
The adapter is a component that stands between the data model we want to show in our app UI and the UI component that renders this information. In other words, an adapter guides the way the information are shown in the UI.
To connect data set and view we need an adapter as you have already used adapter in Listview. But in case of RecyclerView we are not going to extend any base adapter or array adapter like ListView. For RecyclerView we are going to extend RecyclerView.Adapter in our Adapter class like below code. Create an new java class by right click on your package and name it RecyclerAdapter and extend RecyclerView.Adapter it will ask you to implement three method onCreateViewHolder(), onBindViewHolder(), getItemCount().
getItemCount() This method return the number of items present in the data.onCreateViewHolder() Inside this method we specify the layout that each item of the RecyclerView should use. This is done by inflating the layout using LayoutInflater, passing the output to the constructor of the custom ViewHolder.onBindViewHolder() This method is very similar to the getView method of a ListView's adapter. In our example, here's where you have to set the String values to TextView. Now we will complete some small things like we will make an array for title text. We will also make a context variable LayoutInflater and a constructor of Adapter class just copy and paste below code in your adapter class above all the methods.
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerViewHolder>{
String[] name = {"The Shawshank Redemption", "The Godfather",
"The Godfather: Part II", "The Dark Knight", "Schindler's List", "12 Angry Men", "Pulp Fiction"};
Context context;
LayoutInflater inflater;
public RecyclerAdapter(Context context) {
this.context = context;
inflater = LayoutInflater.from(context);
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.item_list, parent, false);
RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
holder.tv1.setText(name[position]);
holder.imageView.setOnClickListener(clickListener);
holder.imageView.setTag(holder);
}
View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
RecyclerViewHolder vholder = (RecyclerViewHolder) v.getTag();
int position = vholder.getPosition();
Toast.makeText(context, "Position is " + position, Toast.LENGTH_LONG).show();
}
};
@Override
public int getItemCount() {
return name.length;
}
}
As you can see we inflate item_list.xml file inside onCreateViewHolder(). Also have made object of RecyclerViewHolder inside onCreateViewHolder and return this object like below code.
Inside onBindViewHolder() method we have set text to TextView from name array and also have made onClickListener for click on ImageView of row of RecyclerView.
And last step is to set number of items of RecyclerView so inside getItemCount() method we have returned number of items of name array.
7. Set RecylerView in MainActivity and complete everything.
Open your MainActivity.java and we will complete following steps
RecyclerView and register.RecyclerAdapter class and set Adapter to RecyclerView.LayoutManager to RecyclerView in my case I am using LinearLayoutManager.Here is complete code of MainActivity.java.
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.rcView);
RecyclerAdapter adapter = new RecyclerAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
// layout manager for RecyclerView
//int columns = 2;
//GridLayoutManager lm = new GridLayoutManager(this, columns,
// GridLayoutManager.VERTICAL, false);
//StaggeredGridLayoutManager lm = new StaggeredGridLayoutManager(columns,
// StaggeredGridLayoutManager.VERTICAL);
//lm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(lm);
}
}
Result

Code on github
Item click listener for RecyclerView
In this section we'll handle user clicks on RecyclerView items. In other words, we want to know if the an user clicks on a item and what type of click he is making: simple click or long click.
When we use RecyclerView the things are a little more complex than ListView. In this case, we have to create a class that implements RecyclerView.OnItemTouchListener.
Right click on your package and create a new java class with name RecyclerItemListener and extends RecyclerView.OnItemTouchListener.
public class RecyclerItemListener
implements RecyclerView.OnItemTouchListener {
private RecyclerTouchListener listener;
private GestureDetector gd;
public interface RecyclerTouchListener {
public void onClickItem(View v, int pos);
public void onLongClickItem(View v, int pos);
}
public RecyclerItemListener(Context ctx, final RecyclerView rv,
final RecyclerTouchListener listener) {
this.listener = listener;
gd = new GestureDetector(ctx,
new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(MotionEvent e) {
View v = rv.findChildViewUnder(e.getX(), e.getY());
listener.onLongClickItem(v, rv.getChildAdapterPosition(v));
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
View v = rv.findChildViewUnder(e.getX(), e.getY());
listener.onClickItem(v, rv.getChildAdapterPosition(v));
return true;
}
});
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
View child = rv.findChildViewUnder(e.getX(), e.getY());
return ( child != null && gd.onTouchEvent(e));
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
}
We define a callback interface to notify the listener when the user clicks on an item. The first thing is creating an instance of GestureDetector so that we can know when the user clicks and what click type he is doing.
Next, our class overrides onInterceptTouchEvent to know if the user selects and item and if the gesture detected is handled by our instance.
To receive notification, in the listener class we simply implements the interface declared above. Following snippet goes to onCreate of MainActivity
recyclerView.addOnItemTouchListener(new RecyclerItemListener(getApplicationContext(),
recyclerView, new RecyclerItemListener.RecyclerTouchListener() {
public void onClickItem(View v, int pos) {
Toast.makeText(getApplicationContext(), "Simple click", Toast.LENGTH_LONG).show();
}
public void onLongClickItem(View v, int position) {
Toast.makeText(getApplicationContext(), "Long click", Toast.LENGTH_LONG).show();
}
}));
Decorate item for RecyclerView
In this section we'll customize the RecyclerView and add a row divider. To do it we have to implement a custom class that extends RecyclerView.ItemDecoration.
Right click on your package and create a new java class with name DividerItemDecoration and extends RecyclerView.ItemDecoration.
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable div;
public DividerItemDecoration(Drawable div) {
this.div = div;
}
@Override
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + div.getIntrinsicHeight();
div.setBounds(left, top, right, bottom);
div.draw(canvas);
}
}
}
In this class, we override onDrawOver and implements our UI customizazion.
To set this decorator add following snippet to onCreate of MainActivity
recyclerView.addItemDecoration(
new DividerItemDecoration(ContextCompat.getDrawable(getApplicationContext(),
R.drawable.item_decorator)));
Where item_decorator is defined under res/drawable directroy in this way
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="#FFB1BEC4" />
</shape>
Also we can add some margin to first item like so
public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
private int offset;
public ItemOffsetDecoration(int offset) {
this.offset = offset;
}
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
if (parent.getChildAdapterPosition(view) == 0) {
outRect.right = offset;
outRect.left = offset;
outRect.top = offset;
outRect.bottom = offset;
}
}
}
}
And use with following statement
recyclerView.addItemDecoration(new ItemOffsetDecoration(20));
You can add horizontal divider to RecyclerView with LinearLayoutManager via following snippet
recyclerView.addItemDecoration(new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL));
There are several ways to create infinite scroll in RecyclerView:
RecyclerView. Tutotial here.RecyclerView functionality. Read about it below.RecyclerView. Tutotial here.setOnLoadMoreListener. Details here.My list of dependencies are
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
compile 'com.android.support:cardview-v7:23.3.0'
compile 'com.github.castorflex.smoothprogressbar:library-circular:1.2.0'
compile 'com.github.markomilos:paginate:0.5.1'
}
Main layout activity_main.xml is the same.
MainActivity have updated to new version
public class MainActivity extends AppCompatActivity implements Paginate.Callbacks {
RecyclerView recyclerView;
private boolean loading = false;
private int currentPage = 0;
protected int threshold = 4;
protected int totalPages = 10;
private Handler handler = new Handler();
private Paginate paginate;
protected boolean addLoadingRow = true;
protected boolean customLoadingListItem = false;
Random rnd = new Random();
RecyclerAdapter moviesAdapter;
ArrayList movies;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.rcView);
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(lm);
recyclerView.addOnItemTouchListener(new RecyclerItemListener(getApplicationContext(), recyclerView,
new RecyclerItemListener.RecyclerTouchListener() {
public void onClickItem(View v, int position) {
Toast.makeText(getApplicationContext(), "On Click Item interface",
Toast.LENGTH_LONG).show();
}
public void onLongClickItem(View v, int position) {
Toast.makeText(getApplicationContext(), "On Long Click Item interface",
Toast.LENGTH_LONG).show();
}
}));
setupPagination();
}
protected void setupPagination() {
if (paginate != null) {
paginate.unbind();
}
handler.removeCallbacks(fakeCallback);
movies = new ArrayList<String>(Arrays.asList("The Shawshank Redemption", "The Godfather",
"The Godfather: Part II", "The Dark Knight", "Schindler's List", "12 Angry Men",
"Pulp Fiction"));
moviesAdapter = new RecyclerAdapter(this, movies);
recyclerView.setAdapter(moviesAdapter);
recyclerView.setHasFixedSize(true);
loading = false;
currentPage = 0;
paginate = Paginate.with(recyclerView, this)
.setLoadingTriggerThreshold(threshold)
.addLoadingListItem(addLoadingRow)
.setLoadingListItemCreator(customLoadingListItem ? new CustomLoadingListItemCreator() : null)
.build();
}
@Override
public synchronized void onLoadMore() {
loading = true;
// fake asynchronous loading that will generate page of random data after some delay
handler.postDelayed(fakeCallback, 1000);
}
@Override
public synchronized boolean isLoading() {
return loading; // return boolean weather data is already loading or not
}
@Override
public boolean hasLoadedAllItems() {
return currentPage == totalPages; // if all pages are loaded return true
}
private Runnable fakeCallback = new Runnable() {
@Override
public void run() {
currentPage++;
moviesAdapter.add("Movie " + String.valueOf(rnd.nextInt(100)));
loading = false;
}
};
private class CustomLoadingListItemCreator implements LoadingListItemCreator {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.custom_loading_list_item, parent, false);
return new LoadingHolder(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
LoadingHolder lh = (LoadingHolder) holder;
lh.tvLoading.setText(String.format("Total items loaded: %d.\nLoading more...", moviesAdapter.getItemCount()));
}
}
static class LoadingHolder extends RecyclerView.ViewHolder {
TextView tvLoading;
public LoadingHolder(View itemView) {
super(itemView);
tvLoading = (TextView) itemView.findViewById(R.id.tv_loading_text);
}
}
}
Note some key things:
Paginate.Callbacks interface for MainActivity.Paginate.Callbacks interface requires three methods onLoadMore, isLoading, hasLoadedAllItems.fakeCallback method for generating new items.CustomLoadingListItemCreator is responsible for loader view.Layout for custom loader is
<?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="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<fr.castorflex.android.circularprogressbar.CircularProgressBar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:indeterminate="true"
app:cpb_color="#F00"
app:cpb_max_sweep_angle="300"
app:cpb_min_sweep_angle="10"
app:cpb_rotation_speed="1.0"
app:cpb_stroke_width="4dp"
app:cpb_sweep_speed="1.0"/>
<TextView
android:id="@+id/tv_loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
tools:text="Loading..."/>
</LinearLayout>
Following is new RecyclerAdapter
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerViewHolder> {
List<String> names;
Context context;
LayoutInflater inflater;
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.item_list, parent, false);
RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
return viewHolder;
}
public RecyclerAdapter(Context context, List<String> name) {
this.context = context;
this.names = name;
inflater = LayoutInflater.from(context);
}
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
holder.tv1.setText(names.get(position));
holder.imageView.setTag(holder);
}
@Override
public int getItemCount() {
return names.size();
}
public void add(String name) {
int previousDataSize = names.size();
names.add(name);
//notifyDataSetChanged();
notifyItemInserted(names.size());
}
}
Layout for RecyclerView items and RecyclerViewHolder are the same.
Result

Similar to displaying items as a list, displaying items as a grid is simple using RecyclerView. In this section we are going to display images and text as grid using RecyclerView.
Our main layout has only one RecyclerView widget in LinearLayout.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rvItems"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="1dp"
/>
</LinearLayout>
The next layout is for RecylerView grid item. It has a Textview child widget to display some text.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="2dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"/>
</LinearLayout>
Following is MainActivity with RecyclerView.Adapter and RecyclerViewHolder.
public class MainActivity extends AppCompatActivity {
public class GridAdapter extends RecyclerView.Adapter<GridAdapter.RecyclerViewHolder>{
Context context;
LayoutInflater inflater;
String[] items;
Random rnd = new Random();
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
public RecyclerViewHolder(View view) {
super(view);
tvTitle = (TextView) view.findViewById(R.id.tvTitle);
}
}
public GridAdapter (Context context, String[] items) {
this.context = context;
this.items = items;
inflater = LayoutInflater.from(context);
}
@Override
public GridAdapter.RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.grid_item, parent, false);
GridAdapter.RecyclerViewHolder viewHolder = new GridAdapter.RecyclerViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(GridAdapter.RecyclerViewHolder holder, final int position) {
final String title = items[position];
int color = getRandomHSVColor();
holder.tvTitle.setBackgroundColor(getLighterColor(color));
holder.tvTitle.setTextColor(getReverseColor(color));
holder.tvTitle.setText(title);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(context, title, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public int getItemCount() {
return items.length;
}
protected int getRandomHSVColor(){
// Generate a random hue value between 0 to 360
int hue = rnd.nextInt(361);
// We make the color depth full
float saturation = 1.0f;
// We make a full bright color
float value = 1.0f;
// We avoid color transparency
int alpha = 255;
// Finally, generate the color
int color = Color.HSVToColor(alpha, new float[]{hue, saturation, value});
// Return the color
return color;
}
protected int getReverseColor(int color){
float[] hsv = new float[3];
Color.RGBToHSV(
Color.red(color), // Red value
Color.green(color), // Green value
Color.blue(color), // Blue value
hsv
);
hsv[0] = (hsv[0] + 180) % 360;
return Color.HSVToColor(hsv);
}
protected int getLighterColor(int color){
float[] hsv = new float[3];
Color.colorToHSV(color,hsv);
hsv[2] = 0.2f + 0.8f * hsv[2];
return Color.HSVToColor(hsv);
}
}
int NUM_COLUMNS = 3;
RecyclerView rvItems;
String[] items = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GridAdapter adapter = new GridAdapter(this, items);
rvItems = (RecyclerView) findViewById(R.id.rvItems);
rvItems.setLayoutManager(new GridLayoutManager(this, NUM_COLUMNS));
rvItems.setAdapter(adapter);
rvItems.setHasFixedSize(true);
}
}
Result

You can use following snippet to set spacing between column
public class MainActivity extends AppCompatActivity {
...
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spacing;
private boolean includeEdge;
public GridSpacingItemDecoration(int spacing, boolean includeEdge) {
this.spacing = spacing;
this.includeEdge = includeEdge;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
int spanCount = layoutManager.getSpanCount();
int position = parent.getChildAdapterPosition(view);
int column = position % spanCount;
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount;
outRect.right = (column + 1) * spacing / spanCount;
if (position < spanCount) {
outRect.top = spacing;
}
outRect.bottom = spacing;
} else {
outRect.left = column * spacing / spanCount;
outRect.right = spacing - (column + 1) * spacing / spanCount;
if (position >= spanCount) {
outRect.top = spacing;
}
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GridAdapter adapter = new GridAdapter(this, items);
rvItems = (RecyclerView) findViewById(R.id.rvItems);
rvItems.setLayoutManager(new GridLayoutManager(this, NUM_COLUMNS));
rvItems.setAdapter(adapter);
rvItems.setHasFixedSize(true);
int spacingInPixels = getResources().getDimensionPixelSize(R.dimen.offset);
GridSpacingItemDecoration itemDecoration = new GridSpacingItemDecoration(spacingInPixels, true);
rvItems.addItemDecoration(itemDecoration);
}
}
Result
How to order items in RecyclerView via SortedList
SortedList is a sorted list implementation that can keep items in order and also notify for changes in the list such that it can be bound to a RecyclerView.Adapter.
It keeps items ordered using the compare(Object, Object) method and uses binary search to retrieve items. If the sorting criteria of your items may change, make sure you call appropriate methods while editing them to avoid data inconsistencies.
You can control the order of items and change notifications via the SortedList.Callback parameter. Callbacks methods can be divided into
boolean areContentsTheSame (T2 oldItem, T2 newItem)boolean areItemsTheSame (T2 item1, T2 item2)int compare (T2 o1, T2 o2)First two methods: areContentsTheSame, areItemsTheSame are basically hashCode and equals implementation. Those are used to determine if object was added or updated and are called after compare method returns "0" in comparison step.
Let's create a list of people and sort by distance field.
public class MainActivity extends AppCompatActivity {
private RecyclerView rvItems;
private LinearLayoutManager llManager;
private SortedListAdapter adapter;
private class Person {
private int id;
private float distance;
public Person(int id, float distance) {
this.id = id;
this.distance = distance;
}
public int getId() {
return id;
}
public float getDistance() {
return distance;
}
@Override
public String toString() {
return "Person: " + getId();
}
}
private class TodoViewHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
Person person;
public TodoViewHolder(View v) {
super(v);
tvTitle = (TextView) v;
}
public void bindTo(Person item) {
person = item;
tvTitle.setText("id: " + item.getId() + ", distance: " + item.getDistance());
}
}
private class SortedListAdapter extends RecyclerView.Adapter<TodoViewHolder> {
SortedList<Person> items;
final LayoutInflater inflater;
public SortedListAdapter(LayoutInflater layoutInflater, Person... persons) {
inflater = layoutInflater;
items = new SortedList<>(Person.class, new SortedListAdapterCallback<Person>(this) {
@Override
public int compare(Person p0, Person p1) {
return Float.compare(p0.getDistance(), p1.getDistance());
}
@Override
public boolean areContentsTheSame(Person oldItem, Person newItem) {
return oldItem.getDistance() == newItem.getDistance();
}
@Override
public boolean areItemsTheSame(Person item1, Person item2) {
return item1.getId() == item2.getId();
}
});
for (Person item : persons) {
items.add(item);
}
}
public void addItem(Person item) {
items.add(item);
}
public void delItem(Person item) {
items.remove(item);
}
public void updateItem(Person item) {
int total = items.size();
int pos = -1;
for (int i = 0; i < total; ++i) {
Person tmp = items.get(i);
if (tmp.getId() == item.getId()) {
pos = i;
break;
}
}
if (pos >= 0) {
items.updateItemAt(pos, item);
}
}
@Override
public TodoViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
return new TodoViewHolder(inflater.inflate(R.layout.sorted_item, parent, false));
}
@Override
public void onBindViewHolder(TodoViewHolder holder, int position) {
holder.bindTo(items.get(position));
}
@Override
public int getItemCount() {
return items.size();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rvItems = (RecyclerView) findViewById(R.id.rvItems);
rvItems.setHasFixedSize(true);
llManager = new LinearLayoutManager(this);
rvItems.setLayoutManager(llManager);
adapter = new SortedListAdapter(getLayoutInflater(),
new Person(1, 34.5f),
new Person(2, 12.5f),
new Person(4, 0.5f),
new Person(5, 0.5f),
new Person(3, 99.5f)
);
rvItems.setAdapter(adapter);
rvItems.setHasFixedSize(true);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
adapter.updateItem(new Person(4, 15.5f));
Toast.makeText(MainActivity.this, "Updated", Toast.LENGTH_SHORT).show();
}
}, 1000);
}
}
Here is activity_main.xml file.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/rvItems"/>
</LinearLayout>
Here is sorted_item.xml file.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Building a RecyclerView with Kotlin
Let's learn building a RecyclerView using Kotlin.
In your build.gradle of your project add the following:
buildscript {
ext.kotlin_version = '1.1.51'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
}
}
Kotlin Android Extensions is a compiler plugin that offers a convenient way of accessing views defined in XML via a property-like syntax.
Now in your build.gradle of your app add the following:
apply plugin: 'kotlin-android-extensions'
...
dependencies {
...
implementation 'com.android.support:recyclerview-v7:27.0.0'
implementation 'com.android.support:design:26.1.0'
implementation 'com.android.support:cardview-v7:26.1.0'
}
Add RecyclerView to your activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rvItems"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
Next step is to create our RecyclerViewAdapter.kt*. It is pretty much same as we usually do in Java.
class ItemAdapter(val items: ArrayList<Item>, val onClick: (Item) -> Unit) : RecyclerView.Adapter<ItemAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.rv_item, parent, false)
return ViewHolder(v, onClick)
}
override fun onBindViewHolder(holder: ItemAdapter.ViewHolder, position: Int) {
holder.bindItems(items[position])
}
override fun getItemCount(): Int {
return items.size
}
class ViewHolder(itemView: View, val onClick: (Item) -> Unit) : RecyclerView.ViewHolder(itemView) {
fun bindItems(item: Item) {
with(itemView) {
itemView.tvName.text = item.name
itemView.setOnClickListener{onClick(item)}
}
}
}
}
with is a useful function included in the standard Kotlin library. It basically receives an object and an extension function as parameters, and makes the object execute the function. This means that all the code we define inside the brackets acts as an extension function of the object we specify in the first parameter, and we can use all its public functions and properties, as well as this. Really helpful to simplify code when we do several operations over the same object.
Following is the layout for item.
<?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/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentPadding="10dp">
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"/>
</android.support.v7.widget.CardView>
Now we will add the data class or you can say POJO class in Kotlin. A data class is a special class in Kotlin that provides you with default behaviors for all your Object methods like toString(), hashCode(), equals() and copy().
data class Item(var name: String)
Now everything is ready. Our adapter, model class, item layout and the main layout is ready. Now we have to attach the adapter to your RecyclerView.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvItems.layoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
rvItems.addItemDecoration(LinearLayoutSpaceItemDecoration(16))
val items = ArrayList<Item>()
items.add(Item("Item 1"))
items.add(Item("Item 2"))
items.add(Item("Item 3"))
items.add(Item("Item 4"))
rvItems.adapter = ItemAdapter(items)
}
}
Let's define LinearLayoutSpaceItemDecoration for space between items.
class LinearLayoutSpaceItemDecoration(var spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
if (outRect != null && parent != null) {
var position = parent.getChildAdapterPosition(view)
outRect.left = spacing
outRect.right = spacing
outRect.bottom = spacing
if (position < 1) outRect.top = spacing
}
}
}
Scroll
recyclerView.scrollToPosition(position);
Smooth scroll
recyclerView.smoothScrollToPosition(position);
Add listener on scrolling
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
// Scrolling up
} else {
// Scrolling down
}
}
});
Adding tools attributes for more pleasant UI developing
Android Studio supports a variety of XML attributes in the tools namespace that enable design-time features (such as which layout to show in a fragment) or compile-time behaviors (such as which shrinking mode to apply to your XML resources). When you build your app, the build tools remove these attributes so there is no effect on your APK size or runtime behavior.
Main layout with tools:menu, tools:listitem and tools:layoutManager.
<FrameLayout
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:menu="menu_test">
<RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_test"
tools:layoutManager="GridLayoutManager"
tools:spanCount="2"/>
</FrameLayout>
Item layout with tools:text.
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@tools:sample/lorem/random"
tools:lines="2"/>
</android.support.v7.widget.CardView>
As you can see, tools: gives you great power to prototype and experiment with your layout without writing a single line of Java or Kotlin code or adding unnecessary states to your view’s XML.
In this section you'll learn how to how to populate an empty RecyclerView list using custom animations.
There are some ways of doing this, e.g.:
ItemAnimatoronBindViewHolder() in the AdapterWe’ll be using a third option, LayoutAnimation. It’s easy and only requires a small amount of code. It’s worth noting that though this tutorial is focused around RecyclerViews, LayoutAnimations can be applied to any subclass of ViewGroup.
So let’s start of by creating the item animation, in this example we’ll go for the Fall Down animation shown below.
Start of by creating the file item_animation_fall_down.xml in res/anim/ and add the following:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/anim_duration_medium">
<translate
android:fromYDelta="-20%"
android:toYDelta="0"
android:interpolator="@android:anim/decelerate_interpolator"
/>
<alpha
android:fromAlpha="0"
android:toAlpha="1"
android:interpolator="@android:anim/decelerate_interpolator"
/>
<scale
android:fromXScale="105%"
android:fromYScale="105%"
android:toXScale="100%"
android:toYScale="100%"
android:pivotX="50%"
android:pivotY="50%"
android:interpolator="@android:anim/decelerate_interpolator"
/>
</set>
The steps above will run together during the animation. Here’s a short explanation of each step:
With the item animation done it’s time to define the layout animation which will apply the item animation to each child in the layout. Create a new file called layout_animation_fall_down.xml in res/anim/ and add the following:
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/item_animation_fall_down"
android:delay="15%"
android:animationOrder="normal"
/>
android:animation="@anim/item_animation_fall_down". Defines which animation to apply to each item in the layout.android:delay="15%". Adds a start delay for each item that’s based on the duration of the item animation. 0% will cause all items in the layout to animate simultaneously, and 100% will let each item finish it’s animation before the next one is started. In this case, 15% of item A’s animation will pass before item B starts its animation.android:animationOrder="normal". There are three types to choose from: normal, reverse and random. This allows to control in which order the content will be animated. Normal follows the natural order of the layout (vertical: top to bottom, horizontal: left to right), Reverse is the opposite of Normal and Random…well Random is random order.A LayoutAnimation can be applied both programmatically and in XML.
int resId = R.anim.layout_animation_fall_down; LayoutAnimationController animation = AnimationUtils.loadLayoutAnimation(ctx, resId); recyclerview.setLayoutAnimation(animation);
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/layout_animation_fall_down"
/>
If you are changing data set or just want to re-run the animation you can do it like this:
private void runLayoutAnimation(final RecyclerView recyclerView) {
final Context context = recyclerView.getContext();
final LayoutAnimationController controller =
AnimationUtils.loadLayoutAnimation(context, R.anim.layout_animation_fall_down);
recyclerView.setLayoutAnimation(controller);
recyclerView.getAdapter().notifyDataSetChanged();
recyclerView.scheduleLayoutAnimation();
}
Expandable RecyclerView Android
First of all, we need to create a model class.
public class Movie {
public int id;
private String name;
private int year;
private List<String> actors;
public Movie(String name, int year, List<String> actors) {
this.name = name;
this.year = year;
this.actors = actors;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public List<String> getActors() {
return actors;
}
public void setActors(List<String> actors) {
this.actors = actors;
}
@Override
public String toString() {
return "Movie{" +
"name='" + name + '\'' +
", year=" + year +
'}';
}
}
Following is a adapter.
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.RecyclerViewHolder>{
Context context;
LayoutInflater inflater;
List<Movie> items;
private SparseIntArray expanded = new SparseIntArray();
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
TextView tvTitle, tvActors;
public RecyclerViewHolder(View itemView) {
super(itemView);
tvTitle = (TextView) itemView.findViewById(R.id.tvTitle);
tvActors = (TextView) itemView.findViewById(R.id.tvActors);
}
}
public MovieAdapter(Context context) {
this.context = context;
this.inflater = LayoutInflater.from(context);
this.items = new ArrayList<>();
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.recycle_item, parent, false);
RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(final RecyclerViewHolder holder, final int position) {
final Movie item = items.get(position);
holder.tvTitle.setText(item.getName());
holder.tvTitle.setTag(holder);
if (expanded.get(position) == 0) {
holder.tvActors.setVisibility(View.GONE);
} else {
Animation slideDown = AnimationUtils.loadAnimation(context, R.anim.slide_down);
holder.tvActors.setVisibility(View.VISIBLE);
holder.tvActors.setText(TextUtils.join(",", item.getActors()));
holder.tvActors.startAnimation(slideDown);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (expanded.get(position) == 0) {
expanded.put(position, 1);
} else {
expanded.put(position, 0);
}
notifyItemChanged(position);
}
});
}
@Override
public int getItemCount() {
return items.size();
}
public void addAll(List<Movie> items) {
this.items.clear();
this.items = items;
notifyDataSetChanged();
}
}
Layout for RecyclerView item.
<?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="wrap_content"
android:padding="5dp">
<TextView
android:id="@+id/tvTitle"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Title"
android:textColor="#000000"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/tvActors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/darker_gray"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
android:layout_below="@id/tvTitle"
android:layout_alignParentLeft="true"
android:layout_marginTop="5dp"
android:visibility="gone" />
</RelativeLayout>
Animation for RecyclerView item (/res/anim/slide_down.xml file).
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="500"
android:fromXScale="1.0"
android:fromYScale="0.0"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
MainActivity
public class MainActivity extends AppCompatActivity {
private AppCompatActivity activity = MainActivity.this;
private String TAG = MainActivity.class.getSimpleName();
String channelId = "1";
String channelName = "Main channel";
RecyclerView recyclerView;
String KEY_REPLY = "key_reply";
public static final int NOTIFICATION_ID = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.rcView);
MovieAdapter adapter = new MovieAdapter(this);
recyclerView.setAdapter(adapter);
List<Movie> movies = new ArrayList<>();
movies.add(new Movie("Movie 1", 2001, Arrays.asList("Actor 1", "Actor 2")));
movies.add(new Movie("Movie 2", 2002, Arrays.asList("Actor 3", "Actor 4")));
movies.add(new Movie("Movie 3", 2003, Arrays.asList("Actor 5")));
adapter.addAll(movies);
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(lm);
recyclerView.getLayoutManager().setMeasurementCacheEnabled(false);
}
}
Layout for MainActivity
<?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">
<android.support.v7.widget.RecyclerView
android:id="@+id/rcView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
Result
Also you can use ExpandableListView.
In this recipe, we will learn how to leverage great things in Kotlin to make RecyclerView much more efficient. We will also be using DiffUtils. It is available from 24.02. According to the documentation:
DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.
The definition is self-explainatory. The notifyDatasetChanged is a very expensive operation of the adapter. The DiffUtils only updates the parts that were changed, unlike notifyDatasetChanged, which updates the whole list.
First, we need to define a list of movies. So, we will first create a data class that takes in name and year of movie:
data class Movie (var name:String, val year:Int)
Next, we will create a list of movies:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val movies = listOf<Movie>(
Movie("The Shawshank Redemption", 1994),
Movie("The Godfather", 1972),
Movie("The Dark Knight", 2008),
Movie("The Godfather: Part II", 1974)
)
var adapter = MovieAdapter()
rvMovies.layoutManager = LinearLayoutManager(this)
rvMovies.adapter = adapter
adapter.items = movies
}
}
Now, we will create an adapter. We will name it MovieAdapter:
class MovieAdapter: RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
var items:List<Movie> by Delegates.observable(emptyList()) {
property, oldValue, newValue ->
notifyChanges(oldValue, newValue)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieAdapter.MovieViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.movie_item, parent, false)
return MovieViewHolder(v)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.name.text = items.get(holder.adapterPosition).name
holder.year.text = items.get(holder.adapterPosition).year.toString()
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int, payloads: MutableList<Any>?) {
if (payloads != null) {
if (payloads.isEmpty())
return onBindViewHolder(holder,position)
else {
val o = payloads.get(0) as Bundle
for (key in o.keySet()) {
when(key) {
"name" -> holder.name.text = o.getString("name")
"year" -> holder.year.text = o.getInt("year").toString()
}
}
}
}
}
inner class MovieViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
var name: TextView = view.findViewById(R.id.tvName)
var year: TextView = view.findViewById(R.id.tvYear)
}
private fun notifyChanges(oldValue: List<Movie>, newValue: List<Movie>) {
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldValue.get(oldItemPosition)
val newItem = newValue.get(newItemPosition)
val bundle= Bundle()
if(!oldItem.name.equals(newItem.name)){
bundle.putString("name", newItem.name)
}
if(oldItem.year != newItem.year){
bundle.putInt("year", newItem.year)
}
if(bundle.size() == 0) return null
return bundle
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldValue.get(oldItemPosition) == newValue.get(newItemPosition)
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldValue.get(oldItemPosition).name.equals(newValue.get(newItemPosition).name)
&& oldValue.get(oldItemPosition).year == newValue.get(newItemPosition).year
}
override fun getOldListSize() = oldValue.size
override fun getNewListSize() = newValue.size
})
diff.dispatchUpdatesTo(this)
}
}
The preceding code is quite standard for the general implementation of RecyclerView, except for the two things.
Another thing is that we have defined the list of Movie in the adapters. The items in the adapter is an observable property. This means the listener gets notified of changes to this property.
Now, whenever we try to assign a value to the items variable, the construct under the { .. } block is run, and we have old and new values to do an operation if we want. In this case, we will do it using the notifyChanges method.
Let's dive into the DiffUtils. The DiffUtils requires two arrays/lists, one of which should be the old list and the other should be the new list.
There are five main functions:
getNewListSize(): This returns the size of the new list.getOldListSize(): This method returns the size of the old list.areItemsTheSame(): This method is used to determine whether two objects represent the same item. If your items have unique ids, this method should check their id equality.areContentsTheSame(): This method is used to determine whether the two objects contain the same data. This method is called by DiffUtil only if areItemsTheSame returns true. In our implementation, we are returning true if both objects have the same name and image.getChangePayload(): When areItemsTheSame() returns true and areContentsTheSame() returns false, then DiffUtils calls this method to get the payload of changes.Finally, after the diff calculation, the DiffUtils object dispatches the changes to the Adapter. To do that, we call the dispatchUpdatesTo method:
diff.dispatchUpdatesTo(this)
To update the changes from the data in the payload, you need to override onBindViewHolder(holder, position, payloads).The changes in the payload are dispatched using the notifyItemRangeChanged method of the adapter.
Following is activity_main.xml.
<?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">
<android.support.v7.widget.RecyclerView
android:id="@+id/rvMovies"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</RelativeLayout>
Following is movie_item.xml.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvName"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Large"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvYear"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
DiffUtil uses the following methods of the RecyclerViewAdapter:
These are less costlier than notifyDataSetChanged since they work on individual operations.
Let’s look at our model class:
public class Movie implements Comparable, Cloneable {
private int id;
private String name;
private int year;
public Movie(int id, String name, int year) {
this.name = name;
this.year = year;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public int compareTo(Object o) {
Movie compare = (Movie) o;
if (compare.getYear() == this.getYear() && compare.getName().equals(this.getName())) {
return 0;
}
return 1;
}
@Override
public Movie clone() {
Movie clone;
try {
clone = (Movie) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return clone;
}
@Override
public String toString() {
return "Movie{" +
"name='" + name + '\'' +
", year=" + year +
'}';
}
}
I’ve implemented the Comparable and Cloneable interfaces.
Following is code for MainActivity
public class MainActivity extends AppCompatActivity {
RecyclerView rv;
List<Movie> movies;
MovieAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rv = findViewById(R.id.rv);
movies = new ArrayList<>();
movies.add(new Movie(0, "The Shawshank Redemption", 1994));
movies.add(new Movie(1, "The Godfather", 1972));
movies.add(new Movie(2,"The Dark Knight", 2008));
movies.add(new Movie(3, "The Godfather: Part II", 1974));
adapter = new MovieAdapter(this, movies);
rv.setAdapter(adapter);
rv.setHasFixedSize(true);
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
rv.setLayoutManager(lm);
}
public void updateList(View v) {
ArrayList<Movie> items = new ArrayList<>();
for (Movie movie : movies) {
items.add(movie.clone());
}
for (Movie movie : items) {
if (movie.getYear() < 2000)
movie.setName(String.format("%s (classic)", movie.getName()));
}
adapter.updateList(items);
}
}
ArrayList are passed by reference. We need to create a deep copy of the ArrayList to prevent the original ArrayList from changing when we modify the new ArrayList. Otherwise, DiffUtil would get two ArrayLists that are just the same.
Following is layout for MainActivity
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btnUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Update"
android:onClick="updateList"/>
</LinearLayout>
Following is RecyclerView.Adapter
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.RecyclerViewHolder> {
Context context;
LayoutInflater inflater;
List<Movie> items;
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
TextView tvMovie, tvYear;
public RecyclerViewHolder(View itemView) {
super(itemView);
tvMovie = itemView.findViewById(R.id.tvMovie);
tvYear = itemView.findViewById(R.id.tvYear);
}
}
public MovieAdapter(Context context, List<Movie> items) {
this.context = context;
this.inflater = LayoutInflater.from(context);
this.items = items;
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.item_list, parent, false);
RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
holder.tvMovie.setText("" + items.get(position).getName());
holder.tvYear.setText("" + items.get(position).getYear());
}
@Override
public int getItemCount() {
return items.size();
}
public void updateList(List<Movie> newList) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MovieDiffCallback(newList, this.items));
this.items.clear();
this.items.addAll(newList);
diffResult.dispatchUpdatesTo(this);
}
}
Following is layout for RecyclerView.Adapter
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/tvMovie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="The Movie" />
<TextView
android:id="@+id/tvYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="15sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvMovie"
tools:text="1992" />
</android.support.constraint.ConstraintLayout>
Following is code for DiffUtil.Callback
public class MovieDiffCallback extends DiffUtil.Callback {
List<Movie> newItems;
List<Movie> oldItems;
private static final String TAG = "MovieDiffCallback";
public MovieDiffCallback(List<Movie> newItems, List<Movie> oldItems) {
this.newItems = newItems;
this.oldItems = oldItems;
}
@Override
public int getOldListSize() {
return oldItems.size();
}
@Override
public int getNewListSize() {
return newItems.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).getId() == newItems.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).equals(newItems.get(newItemPosition));
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
Useful links