Home » Android » android – Implement multiple ViewHolder types in RecycleView adapter

android – Implement multiple ViewHolder types in RecycleView adapter

Posted by: admin May 14, 2020 Leave a comment

Questions:

It’s maybe a discussion not a question.

Normal way to implement multiple types

As you know, if we want to implement multiple types in RecyclerView, we should provide multiple CustomViewHolder extending RecyclerView.ViewHolder.

For exmpale,

class TextViewHolder extends RecyclerView.ViewHolder{
    TextView textView;
}

class ImageViewHolder extends RecyclerView.ViewHolder{
    ImageView imageView;
}

Then we have to override getItemViewType.And in onCreateViewHolder to construct TextViewHolder or ImageViewHolder.

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (viewType == 0) {
        return new ImageViewHolder(mLayoutInflater.inflate(R.layout.item_image, parent, false));
    } else {
        return new TextViewHolder(mLayoutInflater.inflate(R.layout.item_text, parent, false));
    }
} 

Above code is normal but there is a another way.

Another way

I think only one CustomViewHolder is enough.

 class MultipleViewHolder extends RecyclerView.ViewHolder{
    TextView textView;
    ImageView imageView;

    MultipleViewHolder(View itemView, int type){
       if(type == 0){
         textView = (TextView)itemView.findViewById(xx);
       }else{
         imageView = (ImageView)itemView.findViewById(xx);
       }
    }
 }

Which way do you use in your developing work?

How to&Answers:

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.

Answer:

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.

Answer:

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);
    }
}

Answer:

It might not be the answer you’re expecting but here is an example using Epoxy, which really makes your life easier:

First you define your models:

@EpoxyModelClass(layout = R.layout.header_view_model)
public abstract class HeaderViewModel extends EpoxyModel<TextView> {

    @EpoxyAttribute
    String title;

    @Override
    public void bind(TextView view) {
        super.bind(view);
        view.setText(title);
    }

}

@EpoxyModelClass(layout = R.layout.drink_view_model)
public abstract class DrinkViewModel extends EpoxyModel<View> {

    @EpoxyAttribute
    Drink drink;

    @EpoxyAttribute
    Presenter presenter;

    @Override
    public void bind(View view) {
        super.bind(view);

        final TextView title = view.findViewById(R.id.title);
        final TextView description = view.findViewById(R.id.description);

        title.setText(drink.getTitle());
        description.setText(drink.getDescription());
        view.setOnClickListener(v -> presenter.drinkClicked(drink));
    }

    @Override
    public void unbind(View view) {
        view.setOnClickListener(null);
        super.unbind(view);
    }

}

@EpoxyModelClass(layout = R.layout.food_view_model)
public abstract class FoodViewModel extends EpoxyModel<View> {

    @EpoxyAttribute
    Food food;

    @EpoxyAttribute
    Presenter presenter;

    @Override
    public void bind(View view) {
        super.bind(view);

        final TextView title = view.findViewById(R.id.title);
        final TextView description = view.findViewById(R.id.description);
        final TextView calories = view.findViewById(R.id.calories);

        title.setText(food.getTitle());
        description.setText(food.getDescription());
        calories.setText(food.getCalories());
        view.setOnClickListener(v -> presenter.foodClicked(food));
    }

    @Override
    public void unbind(View view) {
        view.setOnClickListener(null);
        super.unbind(view);
    }

}

Then you define your Controller:

public class DrinkAndFoodController extends Typed2EpoxyController<List<Drink>, List<Food>> {

    @AutoModel
    HeaderViewModel_ drinkTitle;

    @AutoModel
    HeaderViewModel_ foodTitle;

    private final Presenter mPresenter;

    public DrinkAndFoodController(Presenter presenter) {
        mPresenter = presenter;
    }

    @Override
    protected void buildModels(List<Drink> drinks, List<Food> foods) {
        if (!drinks.isEmpty()) {
            drinkTitle
                    .title("Drinks")
                    .addTo(this);
            for (Drink drink : drinks) {
                new DrinkViewModel_()
                        .id(drink.getId())
                        .drink(drink)
                        .presenter(mPresenter)
                        .addTo(this);
            }
        }

        if (!foods.isEmpty()) {
            foodTitle
                    .title("Foods")
                    .addTo(this);
            for (Food food : foods) {
                new FoodViewModel_()
                        .id(food.getId())
                        .food(food)
                        .presenter(mPresenter)
                        .addTo(this);
            }
        }
    }
}

Initialize your Controller:

DrinkAndFodController mController = new DrinkAndFoodController(mPresenter);
mController.setSpanCount(1);

final GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 1);
layoutManager.setSpanSizeLookup(mController.getSpanSizeLookup());
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mController.getAdapter());

And finally you can add your data as easily as this:

final List<Drink> drinks = mManager.getDrinks();
final List<Food> foods = mManager.getFoods();
mController.setData(drinks, foods);

You’ll have a list thats looks like:

Drinks
Drink 1
Drink 2
Drink 3
...
Foods
Food1
Food2
Food3
Food4
...

For more informations you can check the wiki.

Answer:

Second one is buggy because when ViewHolders get recycled it produces unexpected behavior. I considered changing visibility during binding but it isn’t performant enough for large amount of Views. Recycler inside RecyclerView stores ViewHolders per type so first way is more performant.

Answer:

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)
    }

}

Answer:

Here you can use Dynamic method dispatch. Below i share my idea.
//Activity Code

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    ArrayList<Object> dataList = new ArrayList<>();
    dataList.add("Apple");
    dataList.add("Orange");
    dataList.add("Cherry");
    dataList.add("Papaya");
    dataList.add("Grapes");
    dataList.add(100);
    dataList.add(200);
    dataList.add(300);
    dataList.add(400);
    ViewAdapter viewAdapter = new ViewAdapter(dataList);
    recyclerView.setAdapter(viewAdapter);

}

}

//Adapter code

public class ViewAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private ArrayList<Object> dataList;
public ViewAdapter(ArrayList<Object> dataList) {
    this.dataList = dataList;
}

@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    BaseViewHolder baseViewHolder;

    if(viewType == 0) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_one,parent,false);
        baseViewHolder  = new ViewHolderOne(view);
    }else  {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_two,parent,false);
        baseViewHolder  = new ViewHolderSecond(view);
    }
    return baseViewHolder;
}

@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
    holder.bindData(dataList.get(position));
}

@Override
public int getItemViewType(int position) {
    Object obj = dataList.get(position);
    int type = 0;
    if(obj instanceof Integer) {
        type = 0;
    }else if(obj instanceof String) {
        type = 1;
    }
    return type;
}

@Override
public int getItemCount() {
    return dataList != null ? dataList.size() : 0;
}

}

//Base View Holder Code.

public abstract class BaseViewHolder<T> extends RecyclerView.ViewHolder {
public BaseViewHolder(View itemView) {
    super(itemView);
}

public abstract void bindData(T data);

}

//View Holder One Source Code.

public class ViewHolderOne extends BaseViewHolder<Integer> {

private TextView txtView;
public ViewHolderOne(View itemView) {
    super(itemView);
    txtView = itemView.findViewById(R.id.txt_number);
}

@Override
public void bindData(Integer data) {
    txtView.setText("Number:" + data);
}

}

//View Holder Two

public class ViewHolderSecond extends BaseViewHolder<String> {

private TextView textView;
public ViewHolderSecond(View itemView) {
    super(itemView);
    textView = itemView.findViewById(R.id.txt_string);
}

@Override
public void bindData(String data) {
    textView.setText("Text:" + data);
}

}

For project Source:
enter link description here

Answer:

I use this approach intensively:
http://frogermcs.github.io/inject-everything-viewholder-and-dagger-2-example/
In short:

  1. Inject map of view holder factories into adapter.
  2. Delegate onCreateViewHolder to injected factories.
  3. Define onBind on similar on base view holder so that you can call it with retrieved data in onBindViewHolder.
  4. Choose factory depending on 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).

Answer:

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);
}
}