You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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());
    }
}

问题原因分析

  1. 数据时序不一致:删除操作触发数据库更新后,LiveData回调给Adapter提交新列表,但RecyclerView可能仍在使用旧位置信息调用getItemViewType。此时tasks列表已替换为更短的新列表,旧位置请求直接导致数组越界。
  2. submitList实现缺陷:直接替换列表后调用notifyDataSetChanged(),若RecyclerView正处于布局/视图回收阶段,会出现旧位置访问新短列表的冲突。
  3. getItemViewType无边界校验:未先判断position是否在当前列表有效范围内,直接执行tasks.get(position)。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 18:34:59