Android分页加载更多,点击列表跳详情报IndexOutOfBoundsException
异常根本原因
核心矛盾是两个独立的帖子列表数据源完全不同步,直接触发越界崩溃:
- 给RecyclerView做渲染的数据源是
MainActivity里的list(元素类型是用于列表展示的Model对象),分页逻辑是把每次接口返回的帖子追加累加,滚动加载后这个列表的长度会从初始10条增长到20条、30条 - 详情页跳转时读取的数据源是
MainActivity.mListPost(元素类型是包含完整帖子内容的WPPost对象),但在getRetrofit()方法里,每次网络请求返回后都是直接用新的接口返回值整体覆盖这个变量,根本没有做累加操作
实际触发崩溃的完整流程:
- 首次进入页面,第一次请求返回10条WPPost,此时
mListPost.size() = 10,list里也同步追加10个对应的Model对象,点击前10条不会出问题 - 滚动到底部触发第二次分页请求,接口返回新的10条WPPost,此时代码直接执行
mListPost = response.body()——mListPost.size()依然是10,但是list是把这10个新的Model追加进去,总长度变成20 - 点击索引为11的新增帖子项时,把position=11传给详情页,详情页用这个索引去查只有10个元素的
mListPost,直接抛出IndexOutOfBoundsException: Index: 11, Size: 10
附带的逻辑问题会放大故障概率:
- 分页加载的状态标志位
loading只在触发加载时置为false,网络请求完成后从来没有重新置回true,会导致多次重复触发分页请求 - 滚动到底部触发加载时,直接新建适配器实例设置给RecyclerView,完全没必要,只要更新原适配器的数据集再刷新即可
- 适配器里点击事件获取position直接用
onBindViewHolder传入的旧position值,在列表刷新、动画过程中会出现位置错位,应该用getAbsoluteAdapterPosition()获取实时位置 getItemViewType()方法直接返回position值,会给RecyclerView缓存机制造成不必要的ViewHolder创建开销- 适配器初始化时把
total_types赋值为初始数据集大小,从来没有更新,属于冗余无用代码 - 循环追加列表数据时,每加一个元素就调用一次
notifyDataSetChanged(),会触发不必要的频繁布局重算
逐模块修复方案
1. 核心数据源同步修复
修改getRetrofit()里的WPPost赋值逻辑,不要直接覆盖mListPost,要做累加:
// 原来的错误赋值:直接覆盖 // mListPost = response.body(); // 修复后:初始化列表后追加数据 if (mListPost == null) { mListPost = new ArrayList<>(); } // 把新返回的帖子追加入全局列表,不要覆盖 mListPost.addAll(response.body());
注意保证列表的Model顺序和mListPost的顺序完全一致,不要有插入、删除等打乱顺序的操作,确保索引一一对应。
2. 分页加载逻辑修复
- 网络请求成功/失败后,把
loading标志位置回true,允许下一次滚动触发加载 - 不要在滚动监听里直接新建适配器,保留全局的adapter实例,加载完成后直接调用
adapter.notifyDataSetChanged()刷新即可 - 循环追加Model对象时,等所有元素追加完成后统一调用一次刷新方法,减少性能损耗
修改后的滚动监听代码示例:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (dy > 0) { visibleItemCount = mLayoutManager.getChildCount(); totalItemCount = mLayoutManager.getItemCount(); pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition(); if (loading) { if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) { loading = false; getRetrofit(); // 不要新建适配器,直接复用已有实例刷新 adapter.notifyDataSetChanged(); } } } } });
在getRetrofit()的回调里,请求完成后补上状态重置:
@Override public void onResponse(Call<List<WPPost>> call, Response<List<WPPost>> response) { // ... 原来的追加逻辑 loading = true; // 请求完成,重置加载状态 } @Override public void onFailure(Call<List<WPPost>> call, Throwable t) { loading = true; // 失败也要重置状态,避免卡死 }
3. 适配器点击逻辑修复
把原来直接用onBindViewHolder传入的旧position的逻辑,替换成实时获取的绝对位置,避免位置错位:
View.OnClickListener clickListener = v -> { // 实时获取当前ViewHolder对应的位置,不要用闭包里捕获的旧position RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder) v.getTag(); int currentPosition = viewHolder.getAbsoluteAdapterPosition(); // 越界判断,避免位置无效时跳转崩溃 if (currentPosition == RecyclerView.NO_POSITION) return; Intent intent = new Intent(mContext, PostDetails.class); intent.putExtra("itemPosition", currentPosition); mContext.startActivity(intent); };
给每个可点击的控件设置tag,绑定到对应的ViewHolder:
@Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { final Model object = dataset.get(position); ImageTypeViewHolder viewHolder = (ImageTypeViewHolder) holder; viewHolder.title.setText(object.title); viewHolder.subtitle.setText(object.subtitle); Glide.with(mContext) .load(object.Image) .dontAnimate() .placeholder(R.drawable.icon) .into(viewHolder.imageView); // 给控件设置tag,关联当前ViewHolder viewHolder.title.setTag(holder); viewHolder.subtitle.setTag(holder); viewHolder.imageView.setTag(holder); // 给所有可点击控件设置统一的点击监听 viewHolder.title.setOnClickListener(clickListener); viewHolder.subtitle.setOnClickListener(clickListener); viewHolder.imageView.setOnClickListener(clickListener); }
同时删除冗余的total_types变量、修改错误的getItemViewType()实现:
// 原来错误的实现:return position; 会导致ViewHolder完全不缓存 @Override public int getItemViewType(int position) { // 所有 item 类型一致,直接返回固定值即可 return Model.IMAGE_TYPE; }
4. 详情页增加防护逻辑
在详情页读取position之前,增加越界判断,避免极端情况下的崩溃:
int position = i.getExtras().getInt("itemPosition"); // 先判断列表是否为空、索引是否在合法范围内 if (MainActivity.mListPost == null || position < 0 || position >= MainActivity.mListPost.size()) { finish(); return; } // 合法范围内再读取内容 Log.e("PostDetails ", "title is " + MainActivity.mListPost.get(position).getTitle().getRendered());
内容的提问来源于stack exchange,提问作者Punjabi Dharti
相关产品推荐
相关产品推荐

