Implement multiple ViewHolder types in RecycleView adapter
Asked Answered
F

9

12

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?

Farthermost answered 24/9, 2017 at 13:51 Comment(6)
for me.. 1st method is more nicer and easy to read..Somerset
While designing your app or any of its components always consider using the SOLID principles.Thema
it might not be the answer you're expecting but have you already considered using Epoxy ? It really makes your life easierBreadnut
Related : How to create RecyclerView with multiple view typeNeusatz
@Breadnut maybe you could add an example answer using Epoxy?Neusatz
@DavidRawson I've added an answer with an example using Epoxy as you suggestedBreadnut
R
34

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.

Rondon answered 27/9, 2017 at 17:52 Comment(6)
Great , the abstract method bind(Item item) is well designed.Farthermost
I've always used that. Well, ALMOST that. Returning the resourceId as the ViewType is just brilliant hahahaha. Truly amazing. Didn't think my adapters could get any better. Thanks for sharing that =)Uncourtly
Brilliant, exactly what I was looking for and nice and clean. I had to cast the viewholder for the override to work - eg MyViewHolder viewHolder = (MyViewHolder) holderWellbeloved
When i implemented this code I'm getting better solution, but there is some issue with the scrolling. I think it is happening when a new view holder is creating. can you please help?Rosenblast
@Rondon How do we we implement internal view click to pass the position in this case? I have asked a question and an addtion to this #57925691 No luck yet.Antipersonnel
I need this. abstract void bind(Object item);Mirabel
L
2

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.

Letitialetizia answered 27/9, 2017 at 12:13 Comment(0)
M
1

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);
    }
}
Mutt answered 28/9, 2017 at 20:22 Comment(0)
B
1

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.

Breadnut answered 29/9, 2017 at 10:11 Comment(0)
A
0

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.

Arrestment answered 24/9, 2017 at 13:59 Comment(5)
I do not agree with you. Because if you pass 0 in this example, the ImageView not be inited.Farthermost
@LiJianixn ImageView will always be there i.e. it will be inflated and take up space in memory. You will only ever hide it which does not and will not destroy the view or open for recycling unless detached from its parent. You could remove ImageView from its Parent but what's the point beside additional load of creating a view without purpose and then deleting it because you never intended to use it.Thema
Why it will always be inflated? If you pass 0, the item view does not have a child as ImageView because the layout.xml does not contains ImageView. But if you pass 1, it will be inflated.Farthermost
Hmm... I see, I assumed you would add ImageView and TextView in the same layout. Still this is a very bad approach for obvious reasons. Check out my earlier comment regarding SOLID design principles.Thema
As you see R.layout.item_image and R.layout.item_text. I will never put them into a same layout.Farthermost
A
0

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

}
Altissimo answered 29/9, 2017 at 8:27 Comment(0)
A
0

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

Anglican answered 29/9, 2017 at 10:41 Comment(0)
U
0

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

Ungrounded answered 29/9, 2017 at 10:57 Comment(0)
G
-1

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);
}
}
Gilbart answered 25/9, 2017 at 21:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.