問題描述
在使用RecyclerView實現列表的時候會有極低的概率出現點擊后數組越界的報錯的問題。
問題原因
請看下面這個幾行在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; }
數組越界的關鍵點就是使用了getAdapterPosition();來獲取點擊的位置。而getAdapterPosition();方法獲取位置有概率在Adapter在刷新視圖的時候返回 -1 這個值。這個時候就會導致數組越界了。
復現問題
為什么要復現問題? 因為我是測試轉開發,個人習慣解決問題的時候同時去找到復現問題的條件。一般正常情況下,使用getAdapterPosition()方法在上面的代碼中點擊后獲取的數據的位置,在人工進行測試的時候是極難復現這個數組越界問題的。只因為你是人類你沒有這個手速,你沒辦法在刷新視圖的一瞬間(只有16ms的時間)去點擊item獲取getAdapterPosition()數據位置,觸發這個bug。所以,此問題一般是Android設備UI線程輕微被堵塞或者在跑monkey的情況下才會出現這個報錯。
但是,從代碼上刻意去制作條件復現這個問題沒這么難,我們可以增加在獲取getAdapterPosition()前面添加一行notifyDataSetChanged() 刷新視圖即可馬上復現此問題。代碼如下:
@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(); //增加這行代碼 mSelectedPosition = viewHolder.getAdapterPosition(); mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition); } } }); return viewHolder; }
解決問題
最主要的還是理解getAdapterPosition() 與 getLayoutPosition() 的區別,根據實際情況使用對應的方法。下面有說明getAdapterPosition() 與 getLayoutPosition() 的區別,這里提供幾種獲取位置的解決辦法的思維。
方式1,如果是在數據很少變化的情況下,將getAdapterPosition()方法替換成getLayoutPosition()方法就可以解決此問題了,代碼如下:
@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; }
方式2,在public void onBindViewHolder(@NonNull ViewHolder holder, int position) {} 方法里實現點擊,因為直接就有position了。但是這種方法缺點是影響性能,onBindViewHolder方法調用的十分頻繁。這里方法里負責給View裝載數據。在滾動的時候會大量觸發,這里頻繁new 一個接口進去不是非常好。
方式3,使用findContainingViewHolder來定位點擊的ViewHolder在獲取位置
/** * 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); }
getAdapterPosition() 與 getLayoutPosition() 的區別
getAdapterPosition 與 getLayoutPosition 方法是google替代 getPosition提供的新api。 你們也可以在Android studio上看他們的注釋了解下google的想法。
個人認為getAdapterPosition() 是提供數據在刷新的時候提供一個-1的返回值,來告知視圖其實正在重新繪制。這個時候點擊位置與你想要的數據正在變化中是不一致的。(因為繪制View與數據位置這2組內容其實是異步的,所以Position理所當然的是不准確的)
而getLayoutPosition()更加簡單暴力,你點擊不會告訴你數據是否正在刷新,始終會返回一個位置值。這個位置值有可能是之前的視圖item位置,也有可能是刷新視圖后的item位置。
那么問題來了,應該如何取舍他們或者在什么情況下使用他們呢?
getLayoutPosition() 更適合在短時間內數據變動少,View刷新不頻繁的情況下使用,或者是固定列表數據,一切簡單化沒這么復雜。
getAdapterPosition() 更適合在頻繁變動數據的情況下使用,指那種數據刷新極快而且是連續刷新的情況下使用,-1的無位置的返回值告訴你視圖正在變化,你需要判斷是否執行這次點擊。如下代碼:
@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
