Android RecyclerView快速滚动触发IndexOutOfBoundsException异常解决方法
快速滚动RecyclerView时触发IndexOutOfBoundsException,缓慢滚动无异常
快速滚动RecyclerView时出现如下崩溃,缓慢滚动则不会触发:
E/AndroidRuntime: FATAL EXCEPTION: main Process: com.freshop.android.adhoc.consumer, PID: 12225 java.lang.IndexOutOfBoundsException: Index: 43, Size: 36 at java.util.ArrayList.get(ArrayList.java:437) at com.freshop.android.consumer.adapter.DepartmentsAdapter$CustomViewHolder$1.onScrollStateChanged(DepartmentsAdapter.java:526) at androidx.recyclerview.widget.RecyclerView.dispatchOnScrollStateChanged(RecyclerView.java:5210) at androidx.recyclerview.widget.RecyclerView.setScrollState(RecyclerView.java:1550) at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5397)
相关代码片段
DepartmentsAdapter的CustomViewHolder实现
public class CustomViewHolder extends RecyclerView.ViewHolder { private final TextView departmentTitle; private final TextView moreProducts; private final RecyclerView mHorizontalRecyclerView; private ProductShowcaseAdapter showcaseAdapter; private final ProgressBar progress; private final FrameLayout item_framelayout; private final LinearLayout pr_row; public CustomViewHolder(View itemView) { super(itemView); pr_row = itemView.findViewById(R.id.pr_row); departmentTitle = itemView.findViewById(R.id.department); moreProducts = itemView.findViewById(R.id.moreProducts); progress = itemView.findViewById(R.id.progress); item_framelayout = itemView.findViewById(R.id.item); mHorizontalRecyclerView = itemView.findViewById(R.id.products_collection_row); } public void bind(final CustomViewHolder holder, final Department department, final int position, final OnItemClickListener listener) { Log.d(TAG,"bind"); if (department == null) { return; } if (department.getDepartmentProducts() != null && department.getDepartmentProducts().size() > 0) { holder.departmentTitle.setText(department.getName()); if (!department.getHasViewMore()) { holder.moreProducts.setVisibility(View.GONE); } else { holder.moreProducts.setVisibility(View.VISIBLE); holder.moreProducts.setText(R.string.lbl_more); holder.moreProducts.setContentDescription(department.getName() + context.get().getString(R.string.lbl_more)); holder.moreProducts.setTextColor(Theme.primaryColor); holder.moreProducts.setOnClickListener(v -> { if(fragment != null) { (((ProductShowcaseFragment)fragment)).updateDataScrollToTopListener(); } listener.onItemClick(null, department, AppConstants.PRODUCTBTNTYPEMORE, null, null, 0, 0); }); } List<Product> products1 = DataHelper.updateProductsWithFavAndListItems(department.getDepartmentProducts(), shoppingListItems); if (showcaseAdapter == null && context != null) { showcaseAdapter = new ProductShowcaseAdapter(context.get(), imageConfig, products1, shoppingListItems, (data, product, holder1, type, pos) -> listener.onItemClick(product, department, type, holder1, data, position, pos), DataHelper::trackUrlOnLoad); mHorizontalRecyclerView.setNestedScrollingEnabled(false); ((SimpleItemAnimator) Objects.requireNonNull(mHorizontalRecyclerView.getItemAnimator())).setSupportsChangeAnimations(false); if (context != null) { LinearLayoutManager llm = new LinearLayoutManager(context.get()); llm.setOrientation(LinearLayoutManager.HORIZONTAL); mHorizontalRecyclerView.setLayoutManager(llm); } mHorizontalRecyclerView.setAdapter(holder.showcaseAdapter); } else { if (showcaseAdapter != null) showcaseAdapter.updateDataSet(products1, shoppingListItems); } holder.item_framelayout.setVisibility(View.VISIBLE); holder.progress.setVisibility(View.GONE); if (products1 != null && products1.size() >= Integer.parseInt(department.getLimit())) { mHorizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (!recyclerView.canScrollHorizontally(1) && !mScrolled) { if (scrollListener != null) { scrollListener.onScrollListener(true, department, position); } } if (recyclerView.getLayoutManager() != null && department.getDepartmentProducts().size() > 0 && ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() >= 0) { if (department.getDepartmentProducts().contains(department.getDepartmentProducts() // 堆栈追踪中的第526行指向此处 .get(((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition()))) { DataHelper.trackUrlOnView(department.getDepartmentProducts().get(((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition())); } } } }); } } } }
DataHelper.trackUrlOnView方法
public static void trackUrlOnView(Product product) { Log.d(TAG,"trackUrlOnView"); if (product == null) return; if (DataHelper.isNullOrEmpty(product.getOnViewProductUrl())) return; if (product.getIsAd() && !product.isHasViewedAd() && !DataHelper.isNullOrEmpty(product.getOnViewProductUrl())) { final Request request = new Request.Builder() .url(product.getOnViewProductUrl()) .build(); new Thread(() -> { try { httpClient.newCall(request).execute(); product.setHasViewedAd(true); } catch (IOException e) { e.printStackTrace(); } }).start(); } }
问题原因
- 重复绑定滚动监听器:每次
bind方法执行时都会给mHorizontalRecyclerView添加新的滚动监听器,导致多个监听器同时生效,快速滚动时重复触发逻辑,加剧索引越界风险。 - 索引未做合法性校验:直接使用
findFirstCompletelyVisibleItemPosition()返回的位置去获取department.getDepartmentProducts()的元素,未校验该索引是否在列表有效范围内。快速滚动时,RecyclerView视图复用,旧监听器持有的department可能已更新,列表大小变化导致索引超出范围。 - 数据引用过期:滚动监听器持有
bind方法传入的旧department引用,而ViewHolder被复用时,数据已更新,监听器仍在使用旧数据进行操作。
解决方案
1. 避免重复绑定滚动监听器
在添加新的滚动监听器前,先清除旧的监听器,防止重复触发:
if (products1 != null && products1.size() >= Integer.parseInt(department.getLimit())) { // 先清除已有监听器 mHorizontalRecyclerView.clearOnScrollListeners(); mHorizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { // ... 原有逻辑 }); }
2. 严格校验索引合法性
在获取产品元素前,检查索引是否在列表的有效范围内:
@Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (!recyclerView.canScrollHorizontally(1) && !mScrolled) { if (scrollListener != null) { scrollListener.onScrollListener(true, department, position); } } LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager == null) return; int visiblePos = layoutManager.findFirstCompletelyVisibleItemPosition(); List<Product> products = department.getDepartmentProducts(); // 校验列表不为空且索引在有效范围内 if (products != null && products.size() > 0 && visiblePos >= 0 && visiblePos < products.size()) { Product product = products.get(visiblePos); DataHelper.trackUrlOnView(product); } }
3. 复用监听器并获取最新数据
将滚动监听器的创建移到ViewHolder构造方法中,通过getAdapterPosition()获取最新位置,再从数据源中获取对应最新数据,避免持有旧引用:
public CustomViewHolder(View itemView) { super(itemView); // ... 其他视图初始化 mHorizontalRecyclerView = itemView.findViewById(R.id.products_collection_row); // 在构造方法中绑定一次监听器 mHorizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); int adapterPos = getAdapterPosition(); // 校验位置合法性 if (adapterPos < 0 || adapterPos >= mDepartments.size()) return; Department currentDept = mDepartments.get(adapterPos); List<Product> products = currentDept.getDepartmentProducts(); LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager == null || products == null || products.size() == 0) return; int visiblePos = layoutManager.findFirstCompletelyVisibleItemPosition(); if (visiblePos >= 0 && visiblePos < products.size()) { Product product = products.get(visiblePos); DataHelper.trackUrlOnView(product); } // 加载更多逻辑 if (!recyclerView.canScrollHorizontally(1) && !mScrolled) { if (scrollListener != null) { scrollListener.onScrollListener(true, currentDept, adapterPos); } } } }); }
4. 优化加载更多标记
mScrolled变量如果是全局变量,会导致多个ViewHolder互相干扰,建议将mScrolled改为ViewHolder的成员变量,避免跨视图复用冲突:
public class CustomViewHolder extends RecyclerView.ViewHolder { private boolean mScrolled = false; // ... 其他成员变量 // 在滚动监听器中使用 if (!recyclerView.canScrollHorizontally(1) && !mScrolled) { mScrolled = true; // 标记已触发加载 if (scrollListener != null) { scrollListener.onScrollListener(true, currentDept, adapterPos); } } }
内容的提问来源于stack exchange,提问作者Hemal Adani
相关产品推荐
相关产品推荐

