Kotlin Todo应用RecyclerView搜索崩溃(IndexOutOfBoundsException)求助
Kotlin Todo应用搜索时RecyclerView崩溃问题修复
崩溃原因分析
抛出的IndexOutOfBoundsException源于RecyclerView的ViewHolder位置与Adapter数据不一致,核心问题:
- 搜索筛选时,Adapter替换为临时过滤列表,但Checkbox点击监听直接在过滤列表上执行
notifyItemMoved,导致原始数据与Adapter数据不同步 - 每次
bind方法都重复设置OnCheckedChangeListener,引发重复监听,加剧位置冲突 - 搜索输入时频繁触发列表更新,与移动任务的异步UI操作冲突
具体修复方案
1. 分离原始数据与过滤数据
在MainActivity中维护原始全量任务列表和过滤后的列表,所有数据修改都基于原始列表,过滤操作仅生成临时列表供Adapter显示。
2. 避免重复设置Checkbox监听
将Checkbox的监听逻辑移到ViewHolder初始化时,通过setTag绑定当前Task,避免每次bind都添加新监听。
3. 任务移动逻辑回调到MainActivity
不在Adapter中直接修改列表和通知RecyclerView,而是通过回调通知MainActivity更新原始列表,再重新执行过滤并刷新Adapter。
4. 添加搜索输入延迟
避免输入字符时频繁触发过滤,减少RecyclerView的频繁更新操作。
修改后的代码
MainActivity
package com.example.todo_app.View import android.os.Bundle import android.os.Handler import android.os.Looper import android.text.Editable import android.text.TextWatcher import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.example.todo_app.Model.Task import com.example.todo_app.ViewModel.TaskViewModel import com.example.todo_app.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private val mainVM = TaskViewModel() private lateinit var binding: ActivityMainBinding // 原始全量任务列表 private val originalTaskList = mutableListOf<Task>() // 过滤后的任务列表 private var filteredTaskList = mutableListOf<Task>() private lateinit var adapter: TaskAdapter private lateinit var recyclerView: RecyclerView private val searchHandler = Handler(Looper.getMainLooper()) private var searchRunnable: Runnable? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // 读取原始列表并初始化过滤列表 originalTaskList.addAll(mainVM.readListFromFile(this, "daneListy") .sortedWith(compareByDescending { !it.status })) filteredTaskList.addAll(originalTaskList) // RecyclerView setup recyclerView = binding.rv adapter = TaskAdapter(filteredTaskList) { task, newPosition -> // 处理任务移动回调 val oldIndex = originalTaskList.indexOf(task) if (oldIndex != -1) { originalTaskList.removeAt(oldIndex) originalTaskList.add(newPosition, task) // 重新过滤并刷新 filterTasks(binding.etSearch.text.toString()) } } recyclerView.adapter = adapter recyclerView.layoutManager = LinearLayoutManager(this) // Search binding.etSearch.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { // 延迟300ms执行搜索,避免频繁触发 searchRunnable?.let { searchHandler.removeCallbacks(it) } searchRunnable = Runnable { filterTasks(s.toString()) } searchHandler.postDelayed(searchRunnable!!, 300) } override fun afterTextChanged(s: Editable?) {} }) binding.btnAdd.setOnClickListener{ if (binding.etAdd.text.isEmpty()) { binding.etAdd.hint = "To pole nie może być puste" } else { val textToAdd = binding.etAdd.text.toString() val newTask = Task(textToAdd, "opis", false) originalTaskList.add(0, newTask) filterTasks(binding.etSearch.text.toString()) binding.etAdd.setText("") } } } private fun filterTasks(query: String) { filteredTaskList.clear() if (query.isEmpty()) { filteredTaskList.addAll(originalTaskList) } else { filteredTaskList.addAll(originalTaskList.filter { it.title.contains(query, ignoreCase = true) }) } adapter.notifyDataSetChanged() } override fun onPause() { super.onPause() mainVM.saveListToFile(this, originalTaskList, "daneListy") } }
TaskAdapter
package com.example.todo_app.View import android.graphics.Paint import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.example.todo_app.databinding.ListItemBinding import com.example.todo_app.Model.Task class TaskAdapter( private var tasks: List<Task>, private val onTaskMove: (Task, Int) -> Unit ) : RecyclerView.Adapter<TaskAdapter.TaskViewHolder>() { fun updateTaskList(newTaskList: List<Task>) { tasks = newTaskList notifyDataSetChanged() } inner class TaskViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) { init { // 仅初始化一次监听 binding.cbTask.setOnCheckedChangeListener { _, isChecked -> val task = binding.cbTask.tag as Task task.status = isChecked if (isChecked) { binding.cbTask.paintFlags = binding.cbTask.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG // 回调到MainActivity处理移动 onTaskMove(task, tasks.size - 1) } else { binding.cbTask.paintFlags = binding.cbTask.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() onTaskMove(task, 0) } } } fun bind(task: Task) { binding.cbTask.text = task.title binding.cbTask.isChecked = task.status // 绑定Task到Tag binding.cbTask.tag = task // 设置初始的删除线状态 if (task.status) { binding.cbTask.paintFlags = binding.cbTask.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG } else { binding.cbTask.paintFlags = binding.cbTask.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder { val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return TaskViewHolder(binding) } override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { val currentTask = tasks[position] holder.bind(currentTask) } override fun getItemCount() = tasks.size }
TaskViewModel与Task类保持不变
内容的提问来源于stack exchange,提问作者Nolifek
相关产品推荐
相关产品推荐

