As you know, if we want to implement multiple types in RecyclerView
I like to use single responsability classes, as logic is not mixed.
Using the second example, you can quickly turn in spaguetti code, and if you like to check nullability, you are forced to declare "everything" as nullable.
I kind of use the first one.
I use a companion object to declare the static fields, which I use in my implementation.
This project was written in kotlin, but here is how I implemented an Adapter:
/**
* Created by Geert Berkers.
*/
class CustomAdapter(
private val objects: List<Any>,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val FIRST_CELL = 0
const val SECOND_CELL = 1
const val THIRD_CELL = 2
const val OTHER_CELL = 3
const val FirstCellLayout = R.layout.first_cell
const val SecondCellLayout = R.layout.second_cell
const val ThirdCellLayout = R.layout.third_cell
const val OtherCellLayout = R.layout.other_cell
}
override fun getItemCount(): Int = 4
override fun getItemViewType(position: Int): Int = when (position) {
objects[0] -> FIRST_CELL
objects[1] -> SECOND_CELL
objects[2] -> THIRD_CELL
else -> OTHER_CELL
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
FIRST_CELL -> {
val view = inflateLayoutView(FirstCellLayout, parent)
return FirstCellViewHolder(view)
}
SECOND_CELL -> {
val view = inflateLayoutView(SecondCellLayout, parent)
return SecondCellViewHolder(view)
}
THIRD_CELL -> {
val view = inflateLayoutView(ThirdCellLayout, parent)
return ThirdCellViewHolder(view)
}
else -> {
val view = inflateLayoutView(OtherCellLayout, parent)
return OtherCellViewHolder(view)
}
}
}
fun inflateLayoutView(viewResourceId: Int, parent: ViewGroup?, attachToRoot: Boolean = false): View =
LayoutInflater.from(parent?.context).inflate(viewResourceId, parent, attachToRoot)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
val itemViewTpe = getItemViewType(position)
when (itemViewTpe) {
FIRST_CELL -> {
val firstCellViewHolder = holder as FirstCellViewHolder
firstCellViewHolder.bindObject(objects[position])
}
SECOND_CELL -> {
val secondCellViewHolder = holder as SecondCellViewHolder
secondCellViewHolder.bindObject(objects[position])
}
THIRD_CELL -> {
val thirdCellViewHolder = holder as ThirdCellViewHolder
thirdCellViewHolder.bindObject(objects[position])
}
OTHER_CELL -> {
// Do nothing. This only displays a view
}
}
}
}
And here is an example of a ViewHolder:
class FirstCellViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindMedication(object: Object) = with(object) {
itemView.setOnClickListener {
openObject(object)
}
}
private fun openObject(object: Object) {
val context = App.instance
val intent = DisplayObjectActivity.intent(context, object)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
}
Personally I like approach suggested by Yigit Boyar in this talk (fast forward to 31:07). Instead of returning a constant int from getItemViewType()
, return the layout id directly, which is also an int and is guaranteed to be unique:
@Override
public int getItemViewType(int position) {
switch (position) {
case 0:
return R.layout.first;
case 1:
return R.layout.second;
default:
return R.layout.third;
}
}
This will allow you to have following implementation in onCreateViewHolder()
:
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(viewType, parent, false);
MyViewHolder holder = null;
switch (viewType) {
case R.layout.first:
holder = new FirstViewHolder(view);
break;
case R.layout.second:
holder = new SecondViewHolder(view);
break;
case R.layout.third:
holder = new ThirdViewHolder(view);
break;
}
return holder;
}
Where MyViewHolder
is an abstract class:
public static abstract class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View itemView) {
super(itemView);
// perform action specific to all viewholders, e.g.
// ButterKnife.bind(this, itemView);
}
abstract void bind(Item item);
}
And FirstViewHolder
is following:
public static class FirstViewHolder extends MyViewHolder {
@BindView
TextView title;
public FirstViewHolder(View itemView) {
super(itemView);
}
@Override
void bind(Item item) {
title.setText(item.getTitle());
}
}
This will make onBindViewHolder()
to be one-liner:
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.bind(dataList.get(holder.getAdapterPosition()));
}
Thus, you'd have each ViewHolder
separated, where bind(Item)
would be responsible to perform actions specific to that ViewHolder
only.
I use both, whatever is better for current task. I do respect the Single Responcibility principle. Each ViewHolder should do one task.
If I have different view holder logic for different item types - I implement different view holders.
If views for some different item types can be cast to same type and used without checks (for example, if list header and list footer are simple but different views) -- there is no sence in creating identical view holders with different views.
That's the point. Different logic - different ViewHolders. Same logic - same ViewHolders.
The ImageView and TextView example. If your view holder has some logic (for example, setting value) and it is different for different view types -- you should not mix them.
This is bad example:
class MultipleViewHolder extends RecyclerView.ViewHolder{
TextView textView;
ImageView imageView;
MultipleViewHolder(View itemView, int type){
super(itemView);
if(type == 0){
textView = (TextView)itemView.findViewById(xx);
}else{
imageView = (ImageView)itemView.findViewById(xx);
}
}
void setItem(Drawable image){
imageView.setImageDrawable(image);
}
void setItem(String text){
textView.setText(text);
}
}
If your ViewHolders don't have any logic, just holding views, it might be OK for simple cases. for example, if you bind views this way:
@Override
public void onBindViewHolder(ItemViewHolderBase holder, int position) {
holder.setItem(mValues.get(position), position);
if (getItemViewType(position) == 0) {
holder.textView.setText((String)mItems.get(position));
} else {
int res = (int)mItems.get(position);
holder.imageView.setImageResource(res);
}
}
I use this approach intensively: http://frogermcs.github.io/inject-everything-viewholder-and-dagger-2-example/ In short:
onCreateViewHolder
to injected factories.onBind
on similar on base view holder so that you can call it with retrieved data in onBindViewHolder
.getItemViewType
(by either instanceOf
or comparing field value).Why?
It cleanly separates every view holder from the rest of app.
If you use autofactory
from google, you can easily inject dependencies required for every view holder. If you need to notify parent of some event, just create new interface, implement it in parent view (activity) and expose it in dagger. (pro tip: instead of initialising factories from their providers, simply specify that each required item's factory depends on factory that autofactory gives you and dagger will provide that for you).
We use it for +15 view holders and adapter has only to grow by ~3 lines for each new added (getItemViewType
checks).
I use 2nd method without conditional, works great with 100+ items in list.
public class SafeHolder extends RecyclerView.ViewHolder
{
public final ImageView m_ivImage;
public final ImageView m_ivRarity;
public final TextView m_tvItem;
public final TextView m_tvDesc;
public final TextView m_tvQuantity;
public SafeHolder(View itemView) {
super(itemView);
m_ivImage =(ImageView)itemView.findViewById(R.id.safeimage_id);
m_ivRarity =(ImageView)itemView.findViewById(R.id.saferarity_id);
m_tvItem = (TextView) itemView.findViewById(R.id.safeitem_id);
m_tvDesc = (TextView) itemView.findViewById(R.id.safedesc_id);
m_tvQuantity = (TextView) itemView.findViewById(R.id.safequantity_id);
}
}