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

Kotlin自定义类无法继承ListAdapter问题求助(附代码)

修复TodoAdapter无法继承ListAdapter的问题

我在Kotlin开发Android待办App入门时,遇到TodoAdapter无法继承ListAdapter的问题,添加基于txt的本地持久化功能前一切正常,以下是相关代码及修复方案:


原代码展示

TodoAdapter类

class TodoAdapter (
    private val todos: MutableList<Todo>
) : ListAdapter<Todo,TodoAdapter.TodoViewHolder>() {

    class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
        return TodoViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_todo,
                parent,
                false
            )
        )
    }

    fun addTodo(todo: Todo) {
        todos.add(todo)
        notifyItemInserted(todos.size - 1)
    }

    fun deleteDoneTodos() {
        todos.removeAll { todo -
            todo.isChecked
        }
        notifyDataSetChanged()
    }

    private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked: Boolean) {
        if (isChecked) {
            tvTodoTitle.paintFlags = tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
        } else{
            tvTodoTitle.paintFlags = tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
        }
    }

    override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
        val curTodo = todos[position]
        holder.itemView.apply {
            tvTodoTitle.text = curTodo.title        //Hier stimmt etwas nicht: tvTodoTitle Import fehlt???
            cbDone.isChecked = curTodo.isChecked
            toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)
            cbDone.setOnCheckedChangeListener{ _, isChecked -
                toggleStrikeThrough(tvTodoTitle, isChecked)
                curTodo.isChecked = !curTodo.isChecked
            }
        }
    }

    override fun getItemCount(): Int {
        return todos.size
    }
}

Todo数据类

data class Todo(
    val title: String,
    var isChecked: Boolean = false
)

MainActivity持久化相关代码

private fun setupInternalStorageRecyclerView() = binding.rvTodoItems.apply {
    adapter = todoAdapter
    layoutManager = rvTodoItems.layoutManager
}

private fun loadTodoItemsFromInternalStorageIntoRecyclerView() {
    lifecycleScope.launch {
        val todoItems = loadTodoItemsFromInternalStorage()
        todoAdapter.submitList(todoItems)
    }

}

private suspend fun loadTodoItemsFromInternalStorage(): List<Todo> {
    return withContext(Dispatchers.IO) {
        val todoItemList: MutableList<Todo> = mutableListOf<Todo>()
        val files = filesDir.listFiles()
        files?.filter { it.canRead() && it.isFile && it.name.endsWith(".txt") }?.map {
            val lines = it.bufferedReader().readLines()
            for (i in lines.indices step 2) {
                todoItemList.add(Todo(lines[i], lines[i+1].toBoolean()))
            }
            todoItemList
        } ?: mutableListOf<Todo>()
    } as MutableList<Todo>
}

private fun saveTodoItemsToInternalStorage(filename: String, todoItems: List<Todo>): Boolean {
    return try{
        openFileOutput("$filename.txt", MODE_PRIVATE).use { stream -
            File(filename).printWriter().use { out -
                for (item in todoItems) {
                    out.println(item.title)
                    out.println(item.isChecked)
                }
            }
        }
        true
    } catch(e: IOException) {
        e.printStackTrace()
        false
    }
}

问题原因及修复方案

核心问题点

  1. ListAdapter强制要求实现DiffUtil.ItemCallback:ListAdapter依赖DiffUtil做高效列表更新,必须传入DiffCallback实现类,原代码未满足该要求
  2. 重复维护列表数据:同时持有自定义todos列表和ListAdapter内置列表,导致数据不一致
  3. Lambda语法错误:多处箭头->被误写为-,引发编译失败
  4. 布局控件未正确引用:tvTodoTitle需确保布局文件存在对应ID,并正确导入

修复后的代码

修复后的TodoAdapter

class TodoAdapter : ListAdapter<Todo, TodoAdapter.TodoViewHolder>(TodoDiffCallback()) {

    class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    // 实现DiffUtil.ItemCallback,ListAdapter的强制要求
    class TodoDiffCallback : DiffUtil.ItemCallback<Todo>() {
        override fun areItemsTheSame(oldItem: Todo, newItem: Todo): Boolean {
            // 建议后续给Todo添加唯一id字段,此处临时用标题+状态组合判断
            return oldItem.title == newItem.title && oldItem.isChecked == newItem.isChecked
        }

        override fun areContentsTheSame(oldItem: Todo, newItem: Todo): Boolean {
            return oldItem == newItem // Data Class自动实现equals,对比所有字段
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
        return TodoViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_todo,
                parent,
                false
            )
        )
    }

    fun addTodo(todo: Todo) {
        val updatedList = currentList.toMutableList().apply { add(todo) }
        submitList(updatedList)
    }

    fun deleteDoneTodos() {
        val filteredList = currentList.filterNot { it.isChecked }
        submitList(filteredList)
    }

    private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked: Boolean) {
        tvTodoTitle.paintFlags = if (isChecked) {
            tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
        } else {
            tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
        }
    }

    override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
        val curTodo = currentList[position]
        holder.itemView.apply {
            tvTodoTitle.text = curTodo.title
            cbDone.isChecked = curTodo.isChecked
            toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)

            cbDone.setOnCheckedChangeListener { _, isChecked ->
                // 用copy创建新对象,符合ListAdapter不可变数据更新原则
                val updatedTodo = curTodo.copy(isChecked = isChecked)
                val updatedList = currentList.toMutableList().apply { this[position] = updatedTodo }
                submitList(updatedList)
                toggleStrikeThrough(tvTodoTitle, isChecked)
            }
        }
    }

    // ListAdapter已实现getItemCount,无需重写
}

修复后的MainActivity持久化代码

1. 初始化Adapter

private val todoAdapter = TodoAdapter() // 不再传入列表参数

2. 修复保存方法

private fun saveTodoItemsToInternalStorage(filename: String, todoItems: List<Todo>): Boolean {
    return try {
        openFileOutput("$filename.txt", MODE_PRIVATE).bufferedWriter().use { out ->
            todoItems.forEach {
                out.write("${it.title}\n")
                out.write("${it.isChecked}\n")
            }
        }
        true
    } catch (e: IOException) {
        e.printStackTrace()
        false
    }
}

3. 修复加载方法

private suspend fun loadTodoItemsFromInternalStorage(): List<Todo> {
    return withContext(Dispatchers.IO) {
        val todoItemList = mutableListOf<Todo>()
        filesDir.listFiles()?.filter { it.canRead() && it.isFile && it.name.endsWith(".txt") }
            ?.firstOrNull()?.let { file ->
                val lines = file.bufferedReader().readLines()
                for (i in lines.indices step 2) {
                    if (i + 1 < lines.size) { // 防止数组越界
                        todoItemList.add(Todo(lines[i], lines[i+1].toBoolean()))
                    }
                }
            }
        todoItemList
    }
}

内容的提问来源于stack exchange,提问作者Bennet Müller

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 17:50:29