Introduction

Kotlin data class copy() performs a shallow copy — it creates a new instance of the data class but copies references to nested objects rather than duplicating them. When nested properties are mutable (like MutableList, MutableMap, or mutable nested data classes), modifying the copy's nested property also modifies the original. This is a common source of bugs in Compose StateFlow state management, where mutating a copied state object's nested list affects the original state, causing unexpected recomposition or lost updates.

Symptoms

  • Modifying copied data class also changes the original
  • StateFlow state update does not trigger recomposition (same object reference)
  • List modifications affect multiple state instances
  • State mutation in reducer pattern overwrites concurrent updates
  • Compose UI does not update after state change

Common Causes

  • copy() performs shallow copy — nested mutable objects are shared
  • Modifying a MutableList inside a copied data class
  • StateFlow state mutated in place instead of creating new instance
  • copy with default parameter not actually changing any property
  • Nested data class copy requires explicit nested copy call

Step-by-Step Fix

  1. 1.Understand shallow copy behavior:
  2. 2.```kotlin
  3. 3.data class User(
  4. 4.val name: String,
  5. 5.val tags: MutableList<String> = mutableListOf()
  6. 6.)

val original = User("John", mutableListOf("admin", "user")) val copied = original.copy()

// WRONG - modifying copy's mutable list also modifies original copied.tags.add("moderator") println(original.tags) // [admin, user, moderator] — original changed! println(copied.tags) // [admin, user, moderator]

// CORRECT - create a new list in the copy val safeCopy = original.copy(tags = original.tags.toMutableList()) safeCopy.tags.add("moderator") println(original.tags) // [admin, user] — original unchanged println(safeCopy.tags) // [admin, user, moderator] ```

  1. 1.Use immutable collections to prevent accidental mutation:
  2. 2.```kotlin
  3. 3.// Prefer immutable types in data classes
  4. 4.data class User(
  5. 5.val name: String,
  6. 6.val tags: List<String> = emptyList() // Immutable List, not MutableList
  7. 7.)

val user = User("John", listOf("admin", "user"))

// To "add" a tag, create a new instance val updatedUser = user.copy(tags = user.tags + "moderator") // user.tags is still [admin, user] // updatedUser.tags is [admin, user, moderator]

// To "remove" a tag val removedUser = user.copy(tags = user.tags - "admin")

// For maps data class Settings( val preferences: Map<String, String> = emptyMap() )

val settings = Settings(mapOf("theme" to "dark")) val updated = settings.copy(preferences = settings.preferences + ("fontSize" to "14")) ```

  1. 1.Deep copy nested data classes:
  2. 2.```kotlin
  3. 3.data class Address(
  4. 4.val street: String,
  5. 5.val city: String
  6. 6.)

data class User( val name: String, val address: Address )

val user = User("John", Address("123 Main St", "Springfield"))

// Shallow copy — address reference is shared val shallowCopy = user.copy()

// Deep copy — copy nested data class too val deepCopy = user.copy( address = user.address.copy(city = "Shelbyville") )

// For multiple levels of nesting data class Order( val id: String, val user: User, val items: List<OrderItem> )

data class OrderItem( val product: String, val quantity: Int, val metadata: MutableMap<String, String> = mutableMapOf() )

// Deep copy order with all nested data val newOrder = order.copy( user = order.user.copy( address = order.user.address.copy(city = "New City") ), items = order.items.map { item -> item.copy(metadata = item.metadata.toMap()) // Immutable copy of map } ) ```

  1. 1.Fix StateFlow state mutation in Compose ViewModels:
  2. 2.```kotlin
  3. 3.// WRONG - mutate state in place, copy doesn't help
  4. 4.data class UiState(
  5. 5.val users: MutableList<User> = mutableListOf(),
  6. 6.val isLoading: Boolean = false
  7. 7.)

class UserViewModel : ViewModel() { private val _state = MutableStateFlow(UiState()) val state = _state.asStateFlow()

fun addUser(user: User) { // WRONG — copy creates new UiState but the list reference is shared _state.value = _state.value.copy(isLoading = false) _state.value.users.add(user) // Mutates the original list! // Compose may NOT recompose because the StateFlow value didn't actually change }

// CORRECT — create entirely new list fun addUser(user: User) { _state.update { currentState -> currentState.copy( users = currentState.users + user // Creates new list ) } } }

// Use StateFlow.update for atomic updates _state.update { state -> state.copy( users = state.users.toMutableList().apply { add(newUser) }, isLoading = false ) } ```

Prevention

  • Use immutable collections (List, Map, Set) in data classes, not mutable variants
  • Use the + and - operators for collection modifications instead of add/remove
  • Use StateFlow.update for atomic state mutations
  • Explicitly copy nested data classes: copy(nested = nested.copy(field = value))
  • Use Kotlin's @Immutable annotation on data classes used as Compose state
  • Write tests that verify original objects are not modified after copy operations
  • Consider using sealed classes for state with distinct variants instead of mutable fields