Why do changes to a list alter another list within ViewModel, too?

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?

  • How come it’s logging Task when your list contains TaskItem?

    – 




  • 1

    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 updating data.tasks once you’ve saved the task list?

    – 

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.

Leave a Comment