I am working on an Todo-list as an android app to get started with Kotlin, but I am running into the problem, that my TodoAdapter class (which is supposed to define what to do with said Todos in a recyclerview as far as I understood?) can't inherit from the ListAdapter class for some reason.
I believe I didn't have the problem before I tried to add persistence to my app by saving to a simple .txt-file as a start. Please have a look at my code below and help me fix my code.
My TodoAdapter class:
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
}
My data class Todo:
data class Todo(
val title: String,
var isChecked: Boolean = false
)
And this is the code in my MainActivity.kt I tried to add persistence with:
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
}
}
I hope this is enough information to help me with, feel free to ask for more information, I will gladly provide it.
First thing's first, one major problem you have, regardless if you're using ListAdapter, is that you are using your adapter to manage your actual data. You must not use an adapter to be the "master copy" of your data, or else your data will be lost the moment the UI is rebuilt for a screen rotation or someone returning to the app after it has been in the background. Your data should be managed by a ViewModel, and the list instance should be passed along to the adapter by the Activity or Fragment. Your functions that modify the list (such as addTodo()
) should be in your ViewModel, not your adapter.
Regarding your specific question, the quick and dirty solution is to inherit from RecyclerView.Adapter instead of ListAdapter:
class TodoAdapter (
private val todos: MutableList<Todo>
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
ListAdapter is more work to set up, but the advantage with it is that it does automatic comparisons on a background thread when you update your list content so it can automatically find exactly what has changed and animate changes to your list for you. If you want to use ListAdapter, you must define a DiffUtil.ItemCallback for it and pass that to its constructor. Typically, your Todo
class would be defined as an immutable (no var
s) data class
and then you could define your callback like:
// Inside your Todo class define:
object DiffCallback: DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
oldItem == newItem
}
And with ListAdapter, you do not use your own list property. You must pass read-only Lists to it via submitList
. So the class definition would look like:
class TodoAdapter: ListAdapter<TodoAdapter.TodoViewHolder>(Todo.DiffCallback) {
There is a lot more you need to understand to use ListAdapter correctly, so you should work through a tutorial if you want to use it. For example: You must not submit mutable Lists to it. It needs a fresh list instance each time you call submitList()
so it can compare the new version to the previous version. Your Todo
class must not have any mutable properties either. Your line val curTodo = todos[position]
would need to be changed to val curTodo = item
.