21

Having a problem when scrolling RecyclerView after scrolling down and up. The idea is to change elements color, but when I scroll down everything is great and when the scroll goes up - the elements, which are shouldn't be colored are changing color.

Here's my adapter:

public class NotificationsAdapter extends RecyclerView.Adapter<NotificationsAdapter.ViewHolder> {

private NotificationData notificationData;
private Context mContext;
private ArrayList<NotificationData> infromationList = new ArrayList<>();


public NotificationsAdapter(Context context, ArrayList<NotificationData> infromationList) {
    this.infromationList = infromationList;
    this.mContext = context;
}


@Override
public NotificationsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    View itemLayoutView;
    ViewHolder viewHolder;

    itemLayoutView = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.notification_single_item, parent, false);
    viewHolder = new ViewHolder(itemLayoutView, viewType);

    return viewHolder;
}

@Override
public void onBindViewHolder(NotificationsAdapter.ViewHolder holder, int position) {

    notificationData = infromationList.get(position);
    holder.notificationDate.setText(convertDate(notificationData.getDate()));
    holder.notificationStatus.setText(notificationData.getNotificationStatus());
    holder.orderDescription.setText(notificationData.getNotificationLabel());

    if ("true".equals(notificationData.getReadStatus())) {
        holder.root.setBackgroundColor(mContext.getResources().getColor(R.color.white));
        holder.notificationStatus.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
    }

}

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

public static class ViewHolder extends RecyclerView.ViewHolder {

    public TextView notificationDate;
    public TextView notificationStatus;
    public TextView orderDescription;
    public LinearLayout root;

    public ViewHolder(View itemView, int position) {
        super(itemView);

        notificationDate = (TextView) itemView.findViewById(R.id.notificationDate);
        notificationStatus = (TextView) itemView.findViewById(R.id.notificationStatus);
        orderDescription = (TextView) itemView.findViewById(R.id.orderDescription);
        root = (LinearLayout) itemView.findViewById(R.id.root);
    }

}

private String convertDate(String date) {
    String convertedDate;

    String[] parts = new String[2];
    parts = date.split("T");
    date = parts[0];

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
    Date testDate = null;
    try {
        testDate = sdf.parse(date);
    }catch(Exception ex){
        ex.printStackTrace();
    }
    SimpleDateFormat formatter = new SimpleDateFormat("dd.mm.yyyy");
    convertedDate = formatter.format(testDate);

    return convertedDate;
}
}
k.nadonenko
  • 618
  • 2
  • 7
  • 23

10 Answers10

46

I had the same problem and the only solution I found for this is:

holder.setIsRecyclable(false);

Your recycler will not recycle anymore so the items will be the same when you scroll, and if you want to delete some item do not use notifyitemRemoved(position), use notifyDataSetChanged() instead.

Andreas
  • 5,050
  • 8
  • 42
  • 51
Jhonatan Sabadi
  • 718
  • 6
  • 13
  • 15
    BEWARE: You should not use either of these advices. As Jhonatan said himself, the views will no longer be recycled, which forfeits the purpose of the recyclerview entirely, and will lead to bad performance. Also using the brute-force notifyDataSetChanged() should always be your last resort - if at all possible, attempt to let the adapter know exactly which items have been updated when removing or adding data - this will not only improve performance, but also let it perform animations appropriately. – jhm Dec 05 '16 at 10:54
  • This worked for me. But after reading @jhm comment I decided to check more answers. Senator's answer also worked for me. – Szymon Sadło Aug 13 '17 at 12:30
  • This worked for me but having performance issues, recycler view seems very laggy. Answer from @Nathan said to add `setHasStableIds(true);` didn't worked for me. How do i solve this issue for better performance ? – xaif Feb 13 '21 at 04:41
  • Not worked for me! – Ankit Lathiya Nov 22 '21 at 09:15
39

Add setHasStableIds(true); in your adapter constructor and Override these two methodes in adapter.

@Override
public long getItemId(int position) {
            return position;
}

@Override
public int getItemViewType(int position) {
       return position;
}
Nathan Teyou
  • 1,988
  • 14
  • 13
6

There is problem in your onBindViewHolder(...), should be:

if ("true".equals(notificationData.getReadStatus())) {
    holder.root.setBackgroundColor(mContext.getResources().getColor(R.color.white));
    holder.notificationStatus.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
}
else {
    holder.root.setBackgroundColor(yourDefaultColor);
    holder.notificationStatus.setTypeface(yourDefaultTypeface);

}
Xcihnegn
  • 11,459
  • 10
  • 32
  • 32
3
@Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
    final UserData userdata = userdataList.get(position);

    holder.setIsRecyclable(false);

    holder.name.setText(userdata.getName());
    holder.active.setChecked(userdata.getActive());

    String userPic = userdata.getPic();


    holder.active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener(){
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked){
            userdata.setActive(isChecked);
        }
    });
}
Keshav Gera
  • 9,806
  • 1
  • 67
  • 48
1

onBindHolder called several times as Recycler View needs a view unless new one. So each time you set visilibity in child views, other views states are also changes.

Whenever you scroll up and down, these views are getting re-drawed with wrong visibility options so always specify both the conditions cause recycler view doesn't know the previous state/conditions/values of our widgets.

Solution :

If in If block you set visibility of any android widget.setVisibility(View.Gone) then in else block you have to set it's visibility opposite value like widget.setVisibility(View.Visible) to overcome the above problem.

 @Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {

    viewHolder.tvName.setText(ModelCategoryProducts.name.get(i));
    viewHolder.tvPrice.setText("Rs."+String.format("%.2f", Float.parseFloat(ModelCategoryProducts.price.get(i))));
    if(ModelCategoryProducts.special_price.get(i).equals("null")) {
        viewHolder.tvSpecialPrice.setVisibility(View.GONE); // here visibility is gone and in else it's opposite visibility i set.
        viewHolder.tvPrice.setTextColor(Color.parseColor("#ff0000"));
        viewHolder.tvPrice.setPaintFlags(0);// here paint flag is 0 and in else it's opposite flag that i want is set.
    }else if(!ModelCategoryProducts.special_price.get(i).equals("null")){
        viewHolder.tvPrice.setTextColor(Color.parseColor("#E0E0E0"));
        viewHolder.tvSpecialPrice.setVisibility(View.VISIBLE);
        viewHolder.tvSpecialPrice.setText("Rs." + String.format("%.2f", Float.parseFloat(ModelCategoryProducts.special_price.get(i))));
        viewHolder.tvPrice.setPaintFlags(viewHolder.tvPrice.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
    }
    if (!ModelCategoryProducts.image_url.get(i).isEmpty()) {
        Picasso.with(context)
                .load(ModelCategoryProducts.image_url.get(i))
                .into(viewHolder.ivProduct);
    }

    viewHolder.setClickListener(new ItemClickListener() {
        @Override
        public void onClick(View view, int position, boolean isLongClick) {
            if (isLongClick) {
//                    Toast.makeText(context, "#" + position + " - " + ModelCategoryProducts.name.get(position) + " (Long click)", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context, "#" + position + " - " + ModelCategoryProducts.name.get(position), Toast.LENGTH_SHORT).show();
                Intent i = new Intent(context, ProductDetail.class);
                i.putExtra("position",position);
                i.putExtra("flagHlvCheck", 5);
                context.startActivity(i);
            }
        }
    });
}
Sachin Mandhare
  • 758
  • 2
  • 7
  • 20
1

Try adding this in the adapter.

@Override
public int getItemViewType(int position)
{
    return position;
}
efr
  • 181
  • 2
  • 10
  • 1
    with this u need to add adapter.setHasStableIds(true) and override fun getItemId(position: Int): Long { return list.get(position).index!! } override fun getItemViewType(position: Int): Int { return list.get(position).index!!.toInt() } instead of position return a unique Long and Integer value – Ashish Choudhary Apr 21 '20 at 15:06
  • Not worked for me! – Ankit Lathiya Nov 22 '21 at 09:16
1

If someone might face issues with some of the fields in the viewholder getting random values, then try to set all the fields with atleast any default value.

minato
  • 1,504
  • 1
  • 13
  • 23
0
 @Override
public DataObjectHolder onCreateViewHolder(ViewGroup parent,
                                           int viewType) {
    View view = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.custom_layout, parent, false);

    DataObjectHolder dataObjectHolder = new DataObjectHolder(view);
    dataObjectHolder.setIsRecyclable(false);

    return dataObjectHolder;
}
Mohammad nabil
  • 990
  • 2
  • 11
  • 23
0

The best way is indicate an ArrayList for example as a Model and have some parameters and define setter and getter for that.

package com.test.mohammaddvi.snappfood.Model;

public class OfferList {
private boolean visibilityOrder;
private int number;

public OfferList(int number, boolean visibilityOrder) {
   this.number=number;
   this.visibilityOrder=visibilityOrder;
}

public boolean isVisibilityOrder() {
    return visibilityOrder;
}

public void setVisibilityOrder(boolean visibilityOrder) {
    this.visibilityOrder = visibilityOrder;
}

public int getNumber() {
    return number;
}

public void setNumber(int number) {
    this.number = number;
}

}

and set the the variables as where you want and for get you must do it in onBindViewHolder of your recyclerview Adapter:

if (offerList.isVisibilityOrder()) {
        holder.foodMinusButton.setVisibility(View.VISIBLE);
        holder.foodOrderNumber.setText(offerList.getNumber() + "");
        holder.foodOrderNumber.setVisibility(View.VISIBLE);
    } else {
        holder.foodMinusButton.setVisibility(View.INVISIBLE);
    }

and indicate it your recyclerview adapter:

public class RecyclerViewMenuFragmentAdapter extends RecyclerView.Adapter<RecyclerViewMenuFragmentAdapter.SingleItemInMenuFragment> {

private ArrayList<Food> foodList;
private Context mContext;
private List<OfferList> offers;

public RecyclerViewMenuFragmentAdapter(ArrayList<Food> foodList, Context mContext, List<OfferList> offers) {
    this.foodList = foodList;
    this.mContext = mContext;
    this.offers = offers;
}
Mohammad Davari
  • 410
  • 3
  • 12
0
class AnyRVAdapter: androidx.recyclerview.widget.RecyclerView.Adapter<AnyRVAdapter.MViewHolder>() {

// put saver outside viewholder
val saveLayId = mutableListOf<Int>()

inner class MViewHolder(itemView: View) :
    androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {       

    fun bindModel(d: TesListModel.MList, position:Int) {

       // concept here
       val showedId= saveLayId.find { s -> s == layoutPosition}
       if (idClicked == null) {            
           // save the layout id
           lyClicked.visibility = View.VISIBLE
           saveLayId.add(layoutPosition)
       } else {
           // remove the layout id
           lyClicked.visibility = View.INVISIBLE
           saveLayId.remove(layoutPosition)
       }         
    }
}

but i think this code is heavy if you use for large data set.

Mohamed Sajjadh
  • 121
  • 2
  • 10
Jetwiz
  • 516
  • 5
  • 14