Android development RecyclerView.Adapter array out-of-bounds problem after clicking and the difference between getAdapterPosition() and getLayoutPosition()…

Problem description

When using RecyclerView to implement a list, there will be a very low probability of an array out-of-bounds error after being clicked.

Cause of the problem

Please look at the following lines of code in RecyclerView.Adapter

 @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mSelectedPosition = viewHolder.getAdapterPosition();
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }

The key point of array out-of-bounds is the use of getAdapterPosition(); to obtain the click position. The getAdapterPosition(); method obtains the position with a probability of returning a value of -1 when the Adapter refreshes the view. This will cause the array to go out of bounds.

Reproduction problem

Why should the problem be reproduced? Because I switched from testing to development, I am used to finding the conditions to reproduce the problem at the same time when solving the problem. Under normal circumstances, using the getAdapterPosition() method to click on the position of the data obtained in the above code is extremely difficult to reproduce this array out-of-bounds problem during manual testing. Just because you are a human and you do not have the hand speed, you cannot click on the item to obtain the getAdapterPosition() data position at the moment of refreshing the view (only 16ms), triggering this bug. Therefore, this problem usually occurs when the UI thread of the Android device is slightly blocked or when running monkey.

However, it is not so difficult to deliberately create conditions in the code to reproduce this problem. We can add a line of notifyDataSetChanged() before getting getAdapterPosition() and refresh the view to reproduce the problem immediately. code show as below:

 @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    notifyDataSetChanged(); //Add this line of code
                    mSelectedPosition = viewHolder.getAdapterPosition();
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }

Solving problems

The most important thing is to understand the difference between getAdapterPosition() and getLayoutPosition(), and use the corresponding method according to the actual situation. The difference between getAdapterPosition() and getLayoutPosition() is explained below. Here are several ideas for solutions to obtain the position.

Method 1, if the data rarely changes, this problem can be solved by replacing the getAdapterPosition() method with the getLayoutPosition() method. The code is as follows:

 @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mSelectedPosition = viewHolder.getLayoutPosition();
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }

Method 2, implement the click in the public void onBindViewHolder(@NonNull ViewHolder holder, int position) {} method, because the position is directly there. However, the disadvantage of this method is that it affects performance, and the onBindViewHolder method is called very frequently. This method is responsible for loading data to the View. A large number of triggers will occur during scrolling. It is not very good to frequently add a new interface here.

Method 3, use findContainingViewHolder to locate the clicked ViewHolder to obtain the position

 /**
     * Returns the ViewHolder that contains the given view.
     *
     * @param view The view that is a descendant of the RecyclerView.
     *
     * @return The ViewHolder that contains the given view or null if the provided view is not a
     * descendant of this RecyclerView.
     */
    @Nullable
    public ViewHolder findContainingViewHolder(@NonNull View view) {
        View itemView = findContainingItemView(view);
        return itemView == null ? null : getChildViewHolder(itemView);
    }

The difference between getAdapterPosition() and getLayoutPosition()

The getAdapterPosition and getLayoutPosition methods are new APIs provided by Google instead of getPosition. You can also read their comments in Android studio to understand Google’s thoughts.

Personally, I think getAdapterPosition() provides a return value of -1 when refreshing the data to inform the view that it is actually being redrawn. At this time, the click position is inconsistent with the changing data you want. (Because the two sets of content of drawing the View and the data position are actually asynchronous, the Position is of course inaccurate)

And getLayoutPosition() is simpler and more violent. When you click, it will not tell you whether the data is being refreshed. It will always return a position value. This position value may be the previous view item position, or it may be the item position after refreshing the view.

So the question is, how should we choose them or under what circumstances should we use them?

getLayoutPosition() is more suitable for use in a short period of time when the data changes little and the View is not refreshed frequently, or when the list data is fixed. Everything is simplified and not so complicated.

getAdapterPosition() is more suitable for use in situations where data changes frequently, when the data is refreshed very quickly and continuously. The positionless return value of -1 tells you that the view is changing, and you need to determine whether to execute this. clicks. The following code:

 @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mSelectedPosition = viewHolder.getAdapterPosition();
                    if (mSelectedPosition == RecyclerView.NO_POSITION){
                        return;
                    }
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }
onBindViewHolder