zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    【Codeforces Round #645 (Div. 2) A】 Park Lighting
    【Codeforces Round #636 (Div. 3) D】Constant Palindrome Sum
    【Codeforces Round #628 (Div. 2) D】Ehab the Xorcist
    【Codeforces Round #642 (Div. 3)】Constructing the Array
    【LeetCode 85】最大矩形(第二遍)
    scrum例会报告+燃尽图02
    scrum例会报告+燃尽图01
    版本控制报告
    Scrum立会报告+燃尽图 07
    Scrum立会报告+燃尽图 05
  • 原文地址:https://www.cnblogs.com/guanxinjing/p/12192114.html
Copyright © 2011-2022 走看看