I have an ArrayList
val tasks = remember { mutableStateListOf<TaskItem>() }
where the TaskItem is
@Keep
@Parcelize
data class Task(
var task: String,
var done: Boolean = false
) : Parcelable
and
@Keep
@Parcelize
data class TaskItem(var id: String, var task: Task) : Parcelable
When I enter the target Screen, pass some data into the tasks
:
LaunchedEffect(key1 = Unit) {
tasks.clear()
tasks.addAll(data.tasks)
Log.wtf("TST_data.tasks", data.tasks.map(TaskItem::task).toString())
// Logs: TST_data.tasks [Task(task=Broccoli 🥦, done=true)]
}
where, the data
is driven by a DataModel calss:
class DataModel(private val application: Application) : AndroidViewModel(application) {
....
val tasks = mutableStateListOf<TaskItem>()
....
}
So far is verything ok, but after making some changes to the tasks
, let’s say I changed the value of done
to the false
and I do it like this:
...
onDone = { index ->
tasks[index] = tasks.onDone(index)
}
...
and here is the onDone
extension:
fun List<TaskItem>.onDone(index: Int): TaskItem {
val item = this[index]
val tmpTask = item.task
return item.copy(task = tmpTask.copy(done = !tmpTask.done))
}
After that I want to save the changes before leaving the screen. To make sure that there are some changes, I compare the lists like this:
if(data.tasks.map(TaskItem::task) != tasks.map(TaskItem::task))
// proceed with saving in Room
else
// a toast massage: No changes made.
The problem is, it goes into else
block, which means there are no changes although I made a change. I tried to log those lists to see the lements before calling the if
statement:
Log.wtf("TST_data.tasks", data.tasks.map(TaskItem::task).toString())
Log.wtf("TST_tasks", tasks.map(TaskItem::task).toString())
Here is the result:
TST_data.tasks [Task(task=Broccoli 🥦, done=false)]
TST_tasks [Task(task=Broccoli 🥦, done=false)]
Chnaged value within tasks
changed also the value in within data.tasks
, which is not possible, since I do note change anything within data.tasks
.
As you can see, that the data.tasks
changes whenever I change tasks
.
This should be impossible! Why do it happen and how do I prevent that chnage?
If you put the same item (reference to an instance of DataModel) into two different lists, both lists are pointing at the same instance. If you modify that instance by changing one of its var
properties, both lists will see the same change because they’re looking at the same instance.
For this reason, you cannot compare old and new versions of your data without doing what is a called a deep copy. It would look something like this to copy each individual instance in the list as you create the new list:
list.map { it.copy() }
Of course, you have to do this before you mutate the items in the list. And it gets considerably more complicated if any of the items inside your model class also have var
properties in them. This is all very error prone, which is why it’s recommended not to use any var
properties inside any of your model classes. They should be immutable classes instead. When you need to change something you replace an item in the list with a copy that has the change applied. This is why Kotlin data classes provide convenient copy()
functions that allow you to change specific property values as you make the copy.
Aside from being error prone, it also breaks Compose’s State functionality for the same reason. State can’t detect changes that are coming from mutated classes because when they are mutated, the information about previous state is lost and cannot be compared for it to be able to detect a change.
How come it’s logging
Task
when your list containsTaskItem
?Even if they are different lists, they hold exactly the same items. If you change an item in one list it changes it also in the other because it points to the same object. You will need to make a deep copy of the list to prevent this
@k314159 I sorry, I missed this in the code:
(data.)tasks.map(TaskItem::task).toString()
. I edited the question, too.@Ivo how does the one list point to another, when they meet each other only once when the data is loaded. Aftert that is the data.tasks never used in the screen. I do not understand. Is there any example, to get it properly understood? Thanks.
It would help if you showed some relevant code that you have in the
if
part (where you commented “// proceed with saving”) – in particular, how are you updatingdata.tasks
once you’ve saved the task list?Show 1 more comment