About the Position

When we use RecyclerView, it is always inevitable to know the location of its ItemView to achieve a variety of needs:

  • Set the click event: We need the position of the Item to obtain the relevant data information corresponding to the View, so as to complete the interactive operation of clicking. For example, when we click on the Item of an Item in a list of goods, we can only get the data information of the Item (such as the ID of the Item) when we know the location of the corresponding Item and jump to the correct product details page.
  • Scroll the list to the specified Item position: This scenario is often used when the selected state of RecyclerView changes, the position of the RecyclerView is rolled so that the currently selected Item can be visible to the user. At this time we need to know the position of Item relative to RecyclerView, it is possible to roll RecyclerView to the right position.

Since location is so important to our daily development, RecyclerView must provide us with an API to get location. Yes, RecyclerView is a way to get a location, and there’s more than one:

  • onBindViewHolder(holder: ViewHolder, position: Int)
  • getAdapterPosition
  • getBindingAdapterPosition
  • getAbsoluteAdapterPosition
  • getLayoutPosition

Are you sure you know exactly what they mean, how they are used, and the difference between them?

The position argument in onBindViewHolder

Usually we bind the data and View in the onBindViewHolder with the postion argument, as follows:

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
    holder.tvNumber.text = "Position: ${list[position]}"
}
Copy the code

Obviously, there’s nothing wrong with that (🐶 to survive).

But it would be a little inappropriate to use the position parameter here to handle the click event, so let’s add a line to the above code:

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
    holder.tvNumber.text = "Position: ${list[position]}"
    holder.itemView.setOnClickListener {
        Toast.makeText(it.context, "Clicked:${list[position]}", Toast.LENGTH_SHORT).show()
    }
}
Copy the code

Then add a “-1” button to the page. The function is simple: remove the first item of the list.

fun removeFirstItem(a){
    list.removeAt(0)
    notifyItemRemoved(0)}Copy the code

Let’s run it and see what it looks like:

As you can see, if the code does what we expect, it will pop up a toast to the position of the clicked location, but when we call the removeFirstItem method to remove the first item from the list, If you click on postion: 1 and click on: 2, the toast display will pop up. This is what happens when you use the position parameter to set the click event directly in the onBindViewHolder.

According to?

The reason is simple: When notifyItem*() is used to delete/add/change any Item in the RecyclerView data, the RecyclerView does not call the onBindViewHolder method on all items to update the position of the Item. It only updates the Position of notifyItem*(), so the displayed data does not correspond to the real data Position.

In fact, the comments in the official source code also emphasize this extra (comments are important ⚠️) :

Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method again if the position of the item changes in the data set unless the item itself is invalidated or the new position cannot be determined. For this reason, you should only use the position parameter while acquiring the related data item inside this method and should not keep a copy of it. If you need the position of an item later on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will have the updated adapter position.

How to solve this problem? In fact, the annotations of the source code also give the solution (annotations are important ⚠️), using getAdapterPosition.

getAdapterPosition

The ViewHolder gives us the getAdapterPosition method to get the position of the ViewHolder. This method always returns the most recent Position of the ViewHolder, which means that if you use the method, even if you call notifyItem*() to delete/add/change the data on RecyclerView, the return Position will ensure that the obtained Position is correct. Interested can follow the above writing method to see the effect of ~

Things worked out… ?

If you’re eager to open Android Studio to verify that getAdapterPosition really works after reading my solution in the last section, I’ll give you a big thumbs up, after all

On paper come zhongjue shallow, must know this to practice.

So you know what I’m going to say: getAdapterPosition() is deprecated, and the website explains it (the website is important ⚠️) :

Roughly translated in my broken English, Google felt that this method would create ambiguity in the case of nested Adapters, Recommend you consider using getBindingAdapterPosition or getAbsoluteAdapterPosition these two methods.

GetAdapterPosition () = getAdapterPosition() = getAdapterPosition() = getAdapterPosition() = getAdapterPosition() = getAdapterPosition() What is a nested Adapter? Good guy, can Adapter still be nested now?

You don’t say, it’s true. If you happen to use Ali open source vLayout, you will not be unfamiliar with the use of Adapter nested Adapter. We all know that for Android, complex Feed flow page, we are basically through the RecyclerView layout to achieve, by rewriting Adapter getItemViewType to distinguish different styles, to achieve different UI logic, for a long time.

That’s always the case, right?

The biggest problem of this writing method for a long time is that the layout of different styles and types are coupled in the same Adapter. With the iteration of the business, this coupling Adapter is likely to become extremely bloated. Moreover, this writing method should always pay attention to data processing and distinguish ViewType, which brings great challenges to future maintenance. Is there a better way?

For the above problems, Ali provides vLayout library to solve, here will not expand, because – it stops maintenance. Google probably saw the pain in developers’ faces when it came to complex page development and maintenance, so they introduced the MergeAdapter, which is basically a container where you can add multiple Adapters. Then set MergeAdapter to RecyclerView Adapter, so as to easily achieve the effect of the layout. This is Google’s official website write Adapter nested Adapter: MergeAdapter may contain multiple developers write Adapter.

In this case, we can cause ambiguity if we continue to call getAdapterPosition, because the program may not know whether you want the relative or absolute position of the ViewHolder.

Relative position & Absolute position? The difference between getBindindAdapterPosition and getAbsoluteAdapterPosition

The relative and absolute positions here are not official names, but are similar concepts that refer to relative and absolute paths in the file system. Let’s give an example of what relative and absolute position are. In the following example, MergeAdapter contains A Adapter and B Adapter. In the display of the page, B is after A. We want to obtain the location of an element b3 in B. The position of B3 in B, I call it the relative position, and the position of B3 in the RecyclerView, I call it the absolute position.

There are two methods provided by the authoritiesgetBindingAdapterPostionwithgetAbsoluteAdapterPositionIs used to get the relative and absolute position of a ViewHolder.

  • GetBindingAdapterPosition will return the ViewHolder relative to its position in the binding of Adapter, namely the relative position.
  • GetAbsoluteAdapterPosition will return the ViewHolder relative to RecyclerView position, namely the absolute position.

Going back to the two typical scenarios of using Position in RecyclerView that we mentioned at the beginning of this article:

Set click event & record, operation RecyclerView scroll state, for the former, we often use getBindingAdapterPostion to get the data item corresponding to ViewHolder, complete click operation.

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
        holder.tvNumber.text = "Position: ${list[position]}"
        holder.itemView.setOnClickListener {
            Toast.makeText(it.context, "Clicked:${list[holder.bindingAdapterPosition]}", Toast.LENGTH_SHORT).show()
        }
    }
Copy the code

As for the latter, obviously, we should use getAbsoluteAdapterPosition to manipulate RecyclerView rolling.

No, of course, if your project using ConcatAdapter, the getBindingAdapterPostion and getAbsoluteAdapterPosition for you, no difference, However, I recommend that you use different methods to obtain the appropriate positional parameters according to the usage scenario, after all, who is to decide whether to use ConcatAdapter in the future?

getLayoutPosition

So what does getLayoutPosition get? In what scenarios do we use this API to get a location?

GetLayoutPosition, as the name implies, gets the position of the ViewHolder in the actual layout. We all know the reality of RecyclerView using LayoutManager to manage data sets. When the developer calls notifyData*() and other methods to tell the RecyclerView to refresh the UI, for performance reasons, the RecyclerView UI will not be refreshed immediately, consistent with the Data, Instead, the relevant layout is lazily updated through LayoutManager — a process that involves a time wait, typically less than 16ms. So, tell from the senses, getLayoutPosition with getAbsoluteAdapterPosition very similar: GetAbsoluteAdapterPosition returns the absolute position relative to the ViewHolder RecyclerView, while getLayoutPosition returns the ViewHolder absolute position relative to the RecyclerView actual layout.

If you change the Adapter data and refresh the view, it will take a while for the layout to update the view. During this time, the position returned by the two methods will be different.

The position in the Adapter cannot be obtained immediately after notifyDataSetChanged, but only after the layout is complete.

For the position of the Layout, after notifyItemInserted, the Layout cannot get the new position immediately because the Layout has not been updated (it takes less than 16ms to refresh the view), so only the old, But the position in the Adapter can immediately get the latest position.

So, for the click event scenario above, it might be better to use getLayoutPosition to get the user’s click position. This way, we can ensure that the user always clicks what he sees (eliminating the 16ms delay). The code can be modified to look like this:

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
        holder.tvNumber.text = "Position: ${list[position]}"
        holder.itemView.setOnClickListener {
            Toast.makeText(it.context, "Clicked:${list[holder.layoutPosition]}", Toast.LENGTH_SHORT).show()
        }
    }
Copy the code

conclusion

  • Source code comments are important
  • Official website documentation is important

In this case, as API callers, what we need to do is to properly read the source code comments, combine them with official documents, correctly understand their respective meanings and possible impacts, and use them reasonably.