Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RecyclerView with GridLayoutManager and Picasso showing wrong image

Update #1

Added hasStableIds(true) and updated Picasso to version 2.5.2. It does not solve the issue.

Reproduction:

RecyclerView with GridLayoutManager (spanCount = 3). List items are CardViews with ImageView inside.

When all the items does not fit the screen calling notifyItemChanged on one item causes more than one calls to onBindViewHolder(). One call is for position from notifyItemChanged others for items not visible on the screen.

Issue:

Sometimes the item at position passed to the notifyItemChanged is loaded with an image belonging to an item that is not on the screen (most likely due to recycling of the view holder - although I would assume that if the item remains in place then the passed viewholder would be the same).

I have found Jake's comment on other issue here about calling load() even if the file/uri is null. Image is loaded on every onBindViewHolder here.

Simple sample app:

git clone https://github.com/gswierczynski/recycler-view-grid-layout-with-picasso.git

Tap on an item calls notifyItemChanged with parameter equal to the position of that item.

Code:

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment())
                    .commit();
        }
    }

    public static class PlaceholderFragment extends Fragment {

        public PlaceholderFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);

            RecyclerView rv = (RecyclerView) rootView.findViewById(R.id.rv);

            rv.setLayoutManager(new GridLayoutManager(getActivity(), 3));
            rv.setItemAnimator(new DefaultItemAnimator());
            rv.setAdapter(new ImageAdapter());

            return rootView;
        }
    }

    private static class ImageAdapter extends RecyclerView.Adapter<ImageViewHolder> implements ClickableViewHolder.OnClickListener {

        public static final String TAG = "ImageAdapter";
        List<Integer> resourceIds = Arrays.asList(
                R.drawable.a0,
                R.drawable.a1,
                R.drawable.a2,
                R.drawable.a3,
                R.drawable.a4,
                R.drawable.a5,
                R.drawable.a6,
                R.drawable.a7,
                R.drawable.a8,
                R.drawable.a9,
                R.drawable.a10,
                R.drawable.a11,
                R.drawable.a12,
                R.drawable.a13,
                R.drawable.a14,
                R.drawable.a15,
                R.drawable.a16,
                R.drawable.a17,
                R.drawable.a18,
                R.drawable.a19,
                R.drawable.a20);

        @Override
        public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
            return new ImageViewHolder(v, this);
        }

        @Override
        public void onBindViewHolder(ImageViewHolder holder, int position) {
            Log.d(TAG, "onBindViewHolder position: " + position + " | holder obj:" + holder.toString());
            Picasso.with(holder.iv.getContext())
                    .load(resourceIds.get(position))
                    .fit()
                    .centerInside()
                    .into(holder.iv);
        }

        @Override
        public int getItemCount() {
            return resourceIds.size();
        }

        @Override
        public void onClick(View view, int position) {
            Log.d(TAG, "onClick position: " + position);
            notifyItemChanged(position);
        }

        @Override
        public boolean onLongClick(View view, int position) {
            return false;
        }
    }

    private static class ImageViewHolder extends ClickableViewHolder {

        public ImageView iv;

        public ImageViewHolder(View itemView, OnClickListener onClickListener) {
            super(itemView, onClickListener);
            iv = (ImageView) itemView.findViewById(R.id.iv);
        }
    }
}

public class ClickableViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
    OnClickListener onClickListener;


    public ClickableViewHolder(View itemView, OnClickListener onClickListener) {
        super(itemView);
        this.onClickListener = onClickListener;
        itemView.setOnClickListener(this);
        itemView.setOnLongClickListener(this);
    }

    @Override
    public void onClick(View view) {
        onClickListener.onClick(view, getPosition());
    }

    @Override
    public boolean onLongClick(View view) {
        return onClickListener.onLongClick(view, getPosition());
    }

    public static interface OnClickListener {
        void onClick(View view, int position);
        boolean onLongClick(View view, int position);
    }
}
like image 696
gswierczynski Avatar asked Mar 27 '15 22:03

gswierczynski


1 Answers

I spent more time than I'd like to admit to work around oddities with RecyclerView and the new adapter that comes with it. The only thing that finally worked for me in terms of correct updates and making sure notifyDataSetChanges and all of its other siblings didn't cause odd behavior was this:

On my adapter, I set

setHasStableIds(true);

In the constructor. I then overrode this method:

@Override
public long getItemId(int position) {
    // return a unique id here
}

And made sure that all my items returned a unique id.

How you achieve this is up to you. For me, the data was supplied from my web service in the form of a UUID and I cheated by converting parts of the UUID to long using this:

SomeContent content = _data.get(position);
Long code = Math.abs(content.getContentId().getLeastSignificantBits());

Obviously this is not a very safe approach but chances are it works for my lists which will contain < 1000 items. So far I haven't run into any trouble with it.

What I recommend is to try this approach and see if it works for you. Since you have an array, getting a unique number for you should be simple. Maybe try returning the position of the actual item (and not the position that is passed in the getItemId()) or create a unique long for each of your records and pass that in.

like image 145
kha Avatar answered Nov 13 '22 03:11

kha