RecyclerView getItemViewType更新数据库时触发IndexOutOfBoundsException
RecyclerView删除条目崩溃问题排查与修复
问题现象
我有一个包含5种viewType的RecyclerView Adapter,支持添加、删除、勾选、取消勾选、隐藏/显示条目等编辑操作。点击删除条目(条目从Room数据库删除,列表通过LiveData+ViewModel通知更新)时,应用大概率(约90%)崩溃,报错如下:
FATAL EXCEPTION: main
Process: com.jakrta.solo, PID: 8708
java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
at java.util.ArrayList.get(ArrayList.java:437)
at com.jakrta.solo.ui.tasks.TasksListAdapter.getItemViewType(TasksListAdapter.java:547)
报错中Index和Size数值始终一致,看起来条目从数据库删除后,getItemViewType仍在使用旧数组长度,尽管Adapter已经收到新数组。
相关核心代码
LiveData观察与Adapter更新
viewModel.observeActiveTasks() .observe(this, tasks -> { activeTasksAdapter.submitList(tasks); if(!(tasks.isEmpty())) activeHeaderAdapter.submitList(true); else activeHeaderAdapter.clearList(); });
TasksListAdapter核心实现
public class TasksListAdapter extends RecyclerView.Adapter<TasksListAdapter.TasksListViewHolder> { public List<Task> tasks= new ArrayList<>(); private int taskOrder; private TasksListViewHolder holder; ItemClicked activity; public TasksListAdapter(ItemClicked activity, int taskOrder) { this.taskOrder= taskOrder; this.activity=activity; } // ViewHolder定义省略... @NonNull @Override public TasksListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case 1: LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view = inflater.inflate(R.layout.task_layout_one, parent, false); return new TasksListViewHolder(view, viewType); case 2: inflater = LayoutInflater.from(parent.getContext()); view = inflater.inflate(R.layout.task_layout_two, parent, false); return new TasksListViewHolder(view, viewType); case 3: inflater = LayoutInflater.from(parent.getContext()); view = inflater.inflate(R.layout.task_layout_three, parent, false); return new TasksListViewHolder(view, viewType); case 4: inflater = LayoutInflater.from(parent.getContext()); view = inflater.inflate(R.layout.task_layout_four, parent, false); return new TasksListViewHolder(view, viewType); case 5: inflater = LayoutInflater.from(parent.getContext()); view = inflater.inflate(R.layout.task_layout_five, parent, false); return new TasksListViewHolder(view, viewType); } return null; } @Override public void onBindViewHolder(@NonNull TasksListViewHolder holder, int position) { this.holder= holder; holder.tvTaskName.setText(tasks.get(position).getName()); holder.cbTask.setChecked(tasks.get(position).getIsCompleted()); if(position == tasks.size()-1) holder.itemView.setBackgroundResource(R.drawable.background_task_list); else holder.itemView.setBackgroundColor(Color.parseColor("#19212188")); if (tasks.get(position).isHasSubTasks()) { if ( position == tasks.size()-1 || tasks.get(position+1).getTaskOrder()<=tasks.get(position).getTaskOrder()) { holder.btArrowUp.setVisibility(View.VISIBLE); holder.btArrowDown.setVisibility(View.GONE); } else { holder.btArrowUp.setVisibility(View.GONE); holder.btArrowDown.setVisibility(View.VISIBLE); } } else { holder.btArrowUp.setVisibility(View.GONE); holder.btArrowDown.setVisibility(View.GONE); } holder.bDelete.setOnClickListener(v -> activity.onItemDeleted(tasks, position)); holder.cbTask.setOnClickListener(v -> { boolean checked = ((AppCompatCheckBox) v).isChecked(); activity.onTtemChecked(position, checked); }); } public void submitList(final List<Task> tasks) { this.tasks.clear(); this.tasks = tasks; notifyDataSetChanged(); } @Override public int getItemCount() { return tasks.size(); } @Override public int getItemViewType(int position) { switch (tasks.get(position).getTaskOrder()) { case 1: return 1; case 2: return 2; case 3: return 3; case 4: return 4; case 5: return 5; default: return -1; } } public interface ItemClicked { void onItemDeleted(List<Task> tasks, int position); void onMultipleItemsDeleted(List<Task> selectedTasksList); void onItemAdded(Task task); void onTtemChecked(int position, boolean isCompleted); void onItemMoved(); void onTtemRestricted(Task task); void onLaterUpdates(); void onHiddenItemsRestored(Task task, int position); void onHiddenItemsRemoved(int position); } }
Fragment中的删除回调
public class TasksFragment extends Fragment implements TasksListAdapter.ItemClicked { // 其他代码省略... public void setTasksList () { taskListsRecyclerView = binding.tasksListRecyclerView; activeHeaderAdapter= new TasksHeaderAdapter(); activeTasksAdapter = new TasksListAdapter(this, taskOrder); completedHeaderAdapter= new TasksHeaderAdapter(); completedTasksAdapter = new TasksListAdapter(this, taskOrder); tasksConcatAdapter= new ConcatAdapter(activeHeaderAdapter, activeTasksAdapter, completedHeaderAdapter, completedTasksAdapter); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity()); taskListsRecyclerView.setLayoutManager(layoutManager); taskListsRecyclerView.setAdapter(tasksConcatAdapter); RecyclerView.RecycledViewPool viewPool = new RecyclerView.RecycledViewPool(); taskListsRecyclerView.setRecycledViewPool(viewPool); viewPool.setMaxRecycledViews(0, 0); } @Override public void onItemDeleted(List<Task> tasks, int position) { viewModel.deleteTaskById(tasks.get(position).getTaskId()); } }
问题原因分析
- 数据时序不一致:删除操作触发数据库更新后,LiveData回调给Adapter提交新列表,但RecyclerView可能仍在使用旧位置信息调用
getItemViewType。此时tasks列表已替换为更短的新列表,旧位置请求直接导致数组越界。 submitList实现缺陷:直接替换列表后调用notifyDataSetChanged(),若RecyclerView正处于布局/视图回收阶段,会出现旧位置访问新短列表的冲突。getItemViewType无边界校验:未先判断position是否在当前列表有效范围内,直接执行tasks.get(position)。- Adapter持有冗余成员:全局
holder成员变量会导致ViewHolder复用错误,增加数据不一致风险。
修复方案
方案1:给getItemViewType添加边界校验
先确保位置合法,避免越界,同时避免返回无效viewType:
@Override public int getItemViewType(int position) { if (position < 0 || position >= tasks.size()) { // 返回一个合法的默认viewType,避免后续onCreateViewHolder返回null return 1; } switch (tasks.get(position).getTaskOrder()) { case 1: return 1; case 2: return 2; case 3: return 3; case 4: return 4; case 5: return 5; default: return 1; // 替换为合法默认类型,不要返回-1 } }
同步修改onCreateViewHolder,确保不会返回null:
@NonNull @Override public TasksListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view; switch (viewType) { case 1: view = inflater.inflate(R.layout.task_layout_one, parent, false); break; case 2: view = inflater.inflate(R.layout.task_layout_two, parent, false); break; case 3: view = inflater.inflate(R.layout.task_layout_three, parent, false); break; case 4: view = inflater.inflate(R.layout.task_layout_four, parent, false); break; case 5: view = inflater.inflate(R.layout.task_layout_five, parent, false); break; default: // 默认加载合法布局,避免返回null view = inflater.inflate(R.layout.task_layout_one, parent, false); break; } return new TasksListViewHolder(view, viewType); }
方案2:优化submitList实现(推荐使用DiffUtil)
使用DiffUtil实现精确局部更新,避免全局刷新的时序问题,同时保证数据原子性:
private List<Task> currentTasks = new ArrayList<>(); public void submitList(List<Task> newTasks) { DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { return currentTasks.size(); } @Override public int getNewListSize() { return newTasks.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { // 用Task的唯一ID判断是否为同一条目 return currentTasks.get(oldItemPosition).getTaskId() == newTasks.get(newItemPosition).getTaskId(); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { // 可根据Task重写的equals方法判断内容是否一致 return currentTasks.get(oldItemPosition).equals(newTasks.get(newItemPosition)); } }); currentTasks.clear(); currentTasks.addAll(newTasks); diffResult.dispatchUpdatesTo(this); }
方案3:删除回调传递唯一ID而非旧列表
修改ItemClicked接口,避免持有旧列表引用:
public interface ItemClicked { // 替换原有onItemDeleted方法 void onItemDeleted(long taskId); // 其他方法不变... }
Adapter中点击事件传递ID:
holder.bDelete.setOnClickListener(v -> { Task task = tasks.get(position); activity.onItemDeleted(task.getTaskId()); });
Fragment中实现回调:
@Override public void onItemDeleted(long taskId) { viewModel.deleteTaskById(taskId); }
额外修复点
- 删除Adapter中的全局
holder成员变量,onBindViewHolder直接使用方法参数中的holder即可。 onBindViewHolder中访问tasks.get(position+1)时添加边界校验:
if (tasks.get(position).isHasSubTasks()) { if (position == tasks.size()-1 || (position+1 < tasks.size() && tasks.get(position+1).getTaskOrder()<=tasks.get(position).getTaskOrder())) { // 原有逻辑 } }
内容的提问来源于stack exchange,提问作者Abdulla Bashir
相关产品推荐
相关产品推荐

