Android开发 RecyclerView.Adapter点击后的数组越界问题 与 getAdapterPosition() 与 getLayoutPosition() 的区别...

本文深入探讨了在使用RecyclerView时出现的数组越界问题,分析了getAdapterPosition()与getLayoutPosition()的区别,提供了三种有效解决方法,帮助开发者避免在数据刷新时触发错误。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题描述

  在使用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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值