问题描述
在使用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