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
varproperties, 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:
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
varproperties in them. This is all very error prone, which is why it’s recommended not to use anyvarproperties 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 convenientcopy()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.