Android multi-element RecyclerView — (Tutorial)
First of all, how great are RecyclerViews? RecyclerViews go a long way in helping to reduce scroll lag on pages with long lists by enforcing the ViewHolder pattern and re-using a view once it has left the visible screen.
In Android, a RecyclerView is one of those core UI elements that everyone should know how to use, but typically a RecyclerView looks like this:
A vertical or horizontal list containing the same repeated item, with different data for each element. This is useful when displaying lists of contacts, products on amazon; the list goes on (pun very much intended).
But take a look at any social media app, Facebook and twitter especially, and you will see your feed dotted with videos, images, status updates, tweets and check ins to name but a few of the many different UI elements on the screen.
So how do they go from a single UI element, to this?
Well I’ll tell you. Firstly, create a new Android project in Android Studio with the Empty Activity template.
In your activity_main.xml
file add a RecyclerView
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingTop="15dp" />
Note: You may need to add the RecyclerView
dependancy to your gradle file
compile 'com.android.support:recyclerview-v7:27.1.0'
Initialise your RecyclerView
in your MainActivity.java
onCreate
method and add its LayoutManager
:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
}
Now it’s time for the fun stuff. All of the work that goes in to displaying the data inside your RecyclerView
is done in the Adapter, but typically your adapter has one ViewHolder
, not today. We’re going to add five ViewHolder
classes and four layout files, one ViewHolder
class and layout file for each element we want to display plus one extra class as a base class the other four will inherit from.
Content.java
public class Content {
//For our purposes this will be a basic object class that only contains the view type we want
private int type;
public Content(int type) {
this.type = type;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
BaseViewHolder.java
public abstract class BaseViewHolder extends RecyclerView.ViewHolder {
public BaseViewHolder(View itemView) {
super(itemView);
}
public abstract void setPayload(Content content);
}
LargeHeaderItemViewHolder.java
public class LargeHeaderItemViewHolder extends BaseViewHolder {
public LargeHeaderItemViewHolder(View itemView) {
super(itemView);
}
@Override
public void setPayload(Content content) {
}
}
view_holder_large_header.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">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:text="This is a large header"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
HeaderItemViewHolder.java
public class HeaderItemViewHolder extends BaseViewHolder{
public HeaderItemViewHolder(View itemView) {
super(itemView);
}
@Override
public void setPayload(Content content) {
}
}
view_holder_header.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">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:text="This is a header"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
ParagraphItemViewHolder.java
public class ParagraphItemViewHolder extends BaseViewHolder {
public ParagraphItemViewHolder(View itemView) {
super(itemView);
}
@Override
public void setPayload(Content content) {
}
}
view_holder_paragraph.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">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:textColor="#A9A9A9"
android:text="This is a paragraph"
android:textSize="12sp" />
</LinearLayout>
ImageItemViewHolder.java
public class ImageItemViewHolder extends BaseViewHolder{
public ImageItemViewHolder(View itemView) {
super(itemView);
}
@Override
public void setPayload(Content content) {
}
}
view_holder_image.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">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_pixelbeard" />
</LinearLayout>
Now we have our RecyclerView
ready and waiting, as well as all of our ViewHolder
's and their corresponding layout files, it’s time to build our Adapter.
Create a new class and call it MultiElementAdapter
:
public class MultiElementAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private List<Content> contentList;
public MultiElementAdapter(List<Content> contents){
this.contentList = contents;
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
@Override
public int getItemViewType(int position) {
return super.getItemViewType(position);
}
}
Now I’m going to break down each method of the adapter one by one, we will start with the constructor:
public MultiElementAdapter(List<Content> contents){
this.contentList = contents;
}
This allows our data of type Content
to be passed in to the adapter.
private static final int LARGE_HEADER = 0;
private static final int HEADER = 1;
private static final int PARAGRAPH = 2;
private static final int IMAGE = 3;@Override
public int getItemViewType(int position) {
switch (contentList.get(position).getType()){
case 0:
return LARGE_HEADER;
case 1:
return HEADER;
case 2:
return PARAGRAPH;
case 3:
return IMAGE;
}
return super.getItemViewType(position);
}
The getItemViewType
method tells the adapter which type of view it should return, this is something we can use in onCreateViewHolder
:
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType){
case LARGE_HEADER:
return new LargeHeaderItemViewHolder(inflater.inflate(R.layout.view_holder_large_header, parent, false));
case HEADER:
return new HeaderItemViewHolder(inflater.inflate(R.layout.view_holder_header, parent, false));
case PARAGRAPH:
return new ParagraphItemViewHolder(inflater.inflate(R.layout.view_holder_paragraph, parent, false));
case IMAGE:
return new ImageItemViewHolder(inflater.inflate(R.layout.view_holder_image, parent, false));
}
return null;
}
In onCreateViewHolder
we take the viewType
and return the relevant ViewHolder
.
Now in onBindViewHolder
we would call the setPayload
method of the ViewHolder
but to keep our example simple we are just going to use static content, but here is an example of how we could implement that functionality:
@Override
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
holder.setPayload(contentList.get(holder.getAdapterPosition()));
}
This could then be used to set dynamic content in the ViewHolder
.
Since all ViewHolder’s inherit from BaseViewHolder
we would only need to do this once, without a switch statement.
Finally we need to return the size of our RecyclerView
:
@Override
public int getItemCount() {
return contentList.size();
}
Now that our adapter is set up, there is only one thing left to do:
List<Content> contents = new ArrayList<>();
contents.add(new Content(0));
contents.add(new Content(1));
contents.add(new Content(1));
contents.add(new Content(2));
contents.add(new Content(2));
contents.add(new Content(3));
contents.add(new Content(3));
contents.add(new Content(2));
contents.add(new Content(1));
MultiElementAdapter adapter = new MultiElementAdapter(contents);
recyclerView.setAdapter(adapter);
We create a dummy list of data with different view types specified (0,1,2,3) and set the adapter, resulting in:
At PixelBeard we have used this method of displaying data multiple times in much more detail, an example of how this can work is in our PlayStation Brand Ambassador app, where users can read articles about the world of PlayStation:
All code available on GitHub at: https://github.com/PixelBeardAgency/MultiElementRecyclerView
In the second part of this tutorial we will expand on the current project by allowing you to display custom content in your multiple view types as well as interacting with your views.
Special mention to Matt Dunn who made this tutorial possible.