RecyclerView onBindViewHolder only called when getItemViewType changes

33,091

Solution 1

I've forgot to implement getItemId when using setHasStableIds(true);

Implementing that solved the issue!

Solution 2

When you set setHasStableIds(true) means that if we request an ID for a location, any specific item will always return the same ID regardless of its position within the list. This enables us to identify a specific item even if its position changes, which will be useful later on.

For that you need to implememt getItemId method in adapter and just return it's positions.

@Override
    public long getItemId(int position) {
        return position;
    }
Share:
33,091

Related videos on Youtube

Richard
Author by

Richard

Updated on January 02, 2020

Comments

  • Richard
    Richard over 4 years

    I have a viewholder with multiple viewtypes.

    When scrolling onBindViewHolder is only called when getItemViewType changes value. This causes my list items to not be updated properly.

    Is this a bug? Or I'm i doing something wrong here. This seems very strange behaviour from the new recyclerView class.

    Here is my adapter:

    package se.davison.smartrecycleradapter;
    
    import android.content.Context;
    import android.support.v7.widget.RecyclerView;
    import android.util.Log;
    import android.util.SparseIntArray;
    import android.view.LayoutInflater;
    import android.view.ViewGroup;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.List;
    
    /**
     * Created by richard on 10/11/14.
     */
    public class SectionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
    
        private static final String TAG = SectionAdapter.class.getSimpleName();
        private final LayoutInflater inflater;
        private final int sectionLayoutId;
        private SparseIntArray positionSection;
        private LinkedHashMap<AdapterItem, List<AdapterItem>> items = new LinkedHashMap<AdapterItem, List<AdapterItem>>();
        private List<Class<? extends AdapterItem>> itemTypes = new ArrayList<Class<? extends AdapterItem>>(20);
    
        public SectionAdapter(Context context, int sectionLayoutId, LinkedHashMap<AdapterItem, List<AdapterItem>> items) {
            this.inflater = LayoutInflater.from(context);
            this.sectionLayoutId = sectionLayoutId;
            this.items = items;
            initList(items);
        }
    
        public SectionAdapter(Context context, int sectionLayoutId) {
            this.inflater = LayoutInflater.from(context);
            this.sectionLayoutId = sectionLayoutId;
            this.items = new LinkedHashMap<AdapterItem, List<AdapterItem>>();
            initList(items);
        }
    
    
        @Override
        public int getItemViewType(int position) {
            AdapterItem item = getItem(position);
            Log.d(TAG, position + ": class " + item.getClass());
            return itemTypes.indexOf(item.getClass());
        }
    
        @Override
        public int getItemCount() {
            int count = 0;
            if (items == null) {
                return 0;
            }
    
            for (AdapterItem key : items.keySet()) {
                count++;
                List<AdapterItem> childItems = items.get(key);
                if (childItems != null) {
                    count += childItems.size();
                }
            }
            return count;
        }
    
        private void initList(HashMap<AdapterItem, List<AdapterItem>> items) {
            positionSection = new SparseIntArray(items.size());
            int count = 0;
            int sectionIndex = -1;
            for (AdapterItem key : items.keySet()) {
                Class headerClass = key.getClass();
                if (!itemTypes.contains(headerClass)) {
                    itemTypes.add(headerClass);
                }
                List<AdapterItem> childItems = items.get(key);
                sectionIndex = count;
                if (childItems != null) {
                    for (AdapterItem item : childItems) {
                        Class clazz = item.getClass();
                        if (!itemTypes.contains(clazz)) {
                            itemTypes.add(clazz);
                        }
                        positionSection.put(count, sectionIndex);
                        count++;
                    }
    
                }
                count++;
            }
            setHasStableIds(true);
        }
    
        private AdapterItem getItem(int position) {
    
            int totalChildCount = 0;
            int separatorCount = 0;
            for (AdapterItem key : items.keySet()) {
                if (position == 0 || position == totalChildCount + separatorCount) {
                    return key;
                }
                separatorCount++;
                List<AdapterItem> list = items.get(key);
                int couldCount = countList(list);
                if (position < totalChildCount + separatorCount + couldCount) {
                    return list.get(position - (totalChildCount + separatorCount));
                }
    
                totalChildCount += couldCount;
            }
    
            return null;
        }
    
        public void setItems(LinkedHashMap<AdapterItem, List<AdapterItem>> items) {
            this.items = items;
            notifyDataSetChanged();
        }
    
        public void setItemsAtHeader(int id, List<AdapterItem> items) {
            AdapterItem header = null;
            for (AdapterItem key : this.items.keySet()) {
                if (key.headerId() == id) {
                    header = key;
                    break;
                }
            }
            if (header == null) {
                throw new IllegalArgumentException(String.format("No header with id %s is found", id));
            }
            setItemsAtHeader(header, items);
        }
    
        private void setItemsAtHeader(AdapterItem header, List<AdapterItem> items) {
            this.items.put(header, items);
        }
    
        private int countList(List<?> list) {
            return list == null ? 0 : list.size();
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
            AdapterItem firstItem = getFirstItemWithClass(itemTypes.get(viewType));
            return firstItem.onCreateViewHolder(inflater, viewGroup);
        }
    
        private AdapterItem getFirstItemWithClass(Class<? extends AdapterItem> clazz) {
    
            for (AdapterItem key : items.keySet()) {
                if (key.getClass() == clazz) {
                    return key;
                }
                List<AdapterItem> childItems = items.get(key);
                if (childItems != null) {
                    for (AdapterItem item : childItems) {
                        if (item.getClass() == clazz) {
                            return item;
                        }
                    }
                }
            }
            throw new IllegalStateException("Something is wrong, you dont have any items with that class in your list");
        }
    
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
            AdapterItem item = getItem(position);
            Log.d(TAG, "ITEM = " + item.getClass().getSimpleName());
            Log.d(TAG, "POS = " + position);
            if (item instanceof OneLineAdapterItem) {
                Log.d(TAG, "TEXT = " + ((OneLineAdapterItem) item).getText());
            }
    
            item.onBindViewHolder(viewHolder, position);
        }
    }
    

    I've also abstracted out the items like so:

    public abstract class AdapterItem<VH extends RecyclerView.ViewHolder> {
    
    
        public boolean isHeader(){
            return false;
        }
    
        public int headerId(){
            return -1;
        }
    
        public abstract VH onCreateViewHolder(LayoutInflater inflater, ViewGroup parent);
    
        public abstract void onBindViewHolder(VH viewHolder, int position);
    }
    

    And for sections

    public class SectionAdapterItem extends AdapterItem<SectionAdapterItem.ViewHolder> {
    
        private String text;
        private boolean dividerVisible = false;
    
        public static class ViewHolder extends RecyclerView.ViewHolder{
    
            TextView titel;
            ImageView divider;
    
            public ViewHolder(View itemView) {
                super(itemView);
                titel = (TextView) itemView.findViewById(android.R.id.title);
                divider = (ImageView) itemView.findViewById(android.R.id.icon);
            }
        }
    
        public void setDividerVisible(boolean dividerVisible) {
            this.dividerVisible = dividerVisible;
        }
    
        public boolean isDividerVisible() {
            return dividerVisible;
        }
    
        @Override
        public boolean isHeader() {
            return true;
        }
    
        @Override
        public int headerId() {
            return super.headerId();
        }
    
        public SectionAdapterItem(String text) {
            this.text = text;
        }
    
        @Override
        public ViewHolder onCreateViewHolder(LayoutInflater inflater, ViewGroup parent) {
            return new ViewHolder(inflater.inflate(R.layout.top_header, parent, false));
        }
    
        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int position) {
            viewHolder.titel.setText(text);
            viewHolder.divider.setVisibility(dividerVisible?View.VISIBLE:View.GONE);
        }
    }
    

    It works fine for the frist visible rows, but then it fails.

    • yigit
      yigit over 9 years
      what do you mean by onBind is never called? What happens instead, it does not show anything? Scrolling stops ?
    • Richard
      Richard over 9 years
      @yigit the first items are showen properly. But when i start scrolling my items are just recycled so their data is not updated since onBind doesn't get called for each row as I scroll. I'll add a few screenshots soon!
    • cV2
      cV2 about 9 years
      for me, this was the problem. stackoverflow.com/a/29162119/371749
  • Dory
    Dory almost 9 years
    Was facing same issue. Saved my day. Tons of thanks..:)
  • Vadim Kotov
    Vadim Kotov almost 9 years
    Worked for me for multiple view types. I wonder why it is not working without stable ids. Does it mean 'multiple view types in adapter' implies 'stabled ids' ?
  • Robin
    Robin almost 9 years
    I met the same problem, even I set has stable ID true and correctly implement the getItemID method. However, it just reduce the frequency of the problem. Some of my item views are still not updated when I scroll the grid. Any idea?
  • Admin
    Admin over 8 years
    Hi, can you put some code?, I don't know where put getItemId, thanks.
  • Richard
    Richard over 8 years
    @delive in your adapter override getItemId and then call myAdapter.setHasStableIds(true) from your activity or fragment or similar
  • Admin
    Admin over 8 years
    getItemID, what value ? Long ? I pass objectsCustoms always, I onlye see post that says ... call setHasStableIds in the constructor (adapter)
  • Sevastyan Savanyuk
    Sevastyan Savanyuk about 7 years
    This helped me to solve the issue that was persisting for several month!