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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 17:04:51