Android開發 RecyclerView.Adapter點擊后的數組越界問題 與 getAdapterPosition() 與 getLayoutPosition() 的區別


問題描述

  在使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM