iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

data class

A Kotlin data class auto-generates equals, hashCode, toString, componentN, and copy. Perfect for DTOs, API responses, value objects.

Generated methods, copy, destructuring

EXAMPLE
// 1) Declare
data class User(
    val id: Long,
    val name: String,
    val email: String,
    val createdAt: Instant = Instant.now(),
)

val u = User(1, "Ada", "a@x.com")
println(u)                            // User(id=1, name=Ada, email=a@x.com, createdAt=...)

// 2) Value equality (NOT identity)
val u1 = User(1, "Ada", "a@x.com")
val u2 = User(1, "Ada", "a@x.com")
println(u1 == u2)                    // true   (value equality)
println(u1 === u2)                   // false  (reference equality)
println(u1.hashCode() == u2.hashCode())  // true

// 3) copy — non-destructive mutation
val older = u.copy(name = "Ada L.")
val renamed = u.copy(email = "ada@example.com")
// u is unchanged.

// 4) componentN + destructuring
val (id, name, email) = u                    // destructures via component1(), component2(), ...
println("\${id} \${name}")

// In a loop
val users = listOf(User(1, "Ada", "a@x"), User(2, "Bo", "b@x"))
for ((id, name, _) in users) {
    println("\${id}: \${name}")
}

// 5) Data class restrictions
// - Must have at least ONE primary constructor parameter
// - All primary ctor params must be val or var
// - Can't be abstract, open, sealed, or inner
// - You CAN add methods + companions + body code beyond the ctor

data class Money(val amount: Double, val currency: String) {
    val display: String get() = "%.2f %s".format(amount, currency)
    fun convert(rate: Double, currency: String) = Money(amount * rate, currency)

    companion object {
        val ZERO_AUD = Money(0.0, "AUD")
    }
}

val m = Money(9.99, "AUD")
println(m.display)                    // 9.99 AUD

// 6) Override componentN / equals / hashCode if needed
data class CaseInsensitiveTag(val value: String) {
    override fun equals(other: Any?): Boolean {
        if (other !is CaseInsensitiveTag) return false
        return value.lowercase() == other.value.lowercase()
    }
    override fun hashCode(): Int = value.lowercase().hashCode()
}

// 7) Data classes + JSON (kotlinx.serialization)
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@@Serializable
data class Post(
    val id: Long,
    val title: String,
    val body: String,
    val tags: List<String> = emptyList(),
)

val p = Post(1, "Hi", "first", listOf("intro"))
val json = Json.encodeToString(Post.serializer(), p)
val back = Json.decodeFromString(Post.serializer(), json)

// 8) Used as map keys
val prices = mutableMapOf<Money, Int>()
prices[Money(9.99, "AUD")] = 100
prices[Money(9.99, "AUD")]                  // 100 — value equality

// 9) Generic data class
data class Result<T>(val value: T, val error: String? = null) {
    val isOk: Boolean get() = error == null
}

fun <T> success(value: T) = Result(value)
fun <T> failure(message: String) = Result<T>(value = null as T, error = message)

// 10) Sealed class with data class variants (discriminated union)
sealed class ApiResult<out T> {
    data class Success<T>(val data: T)        : ApiResult<T>()
    data class Failure(val code: Int, val message: String) : ApiResult<Nothing>()
    object Loading                            : ApiResult<Nothing>()
}

fun <T> handle(r: ApiResult<T>) = when (r) {
    is ApiResult.Success -> println(r.data)
    is ApiResult.Failure -> println("err \${r.code}: \${r.message}")
    ApiResult.Loading    -> println("loading")
}

// 11) Compare with regular class
class User2(
    val id: Long,
    val name: String,
)
val r1 = User2(1, "Ada")
val r2 = User2(1, "Ada")
println(r1 == r2)                    // false — default equals is reference equality

// 12) Nested data classes
data class Order(
    val id: String,
    val customer: Customer,
    val items: List<OrderItem>,
)

data class Customer(val id: Long, val name: String, val email: String)
data class OrderItem(val sku: String, val qty: Int, val price: Double)

val o = Order("o_1", Customer(1, "Ada", "a@x"), listOf(OrderItem("A100", 2, 9.99)))

// Deep copy via copy()
val discounted = o.copy(
    items = o.items.map { it.copy(price = it.price * 0.9) }
)

// 13) Default values for ctor params
data class Settings(
    val theme: String = "auto",
    val notifications: Boolean = true,
    val pageSize: Int = 25,
)

val s = Settings()                                    // all defaults
val s2 = Settings(theme = "dark")                     // override one
val s3 = Settings(pageSize = 50, notifications = false)

// 14) Convert between data classes
data class DomainUser(val id: Long, val name: String)
data class ApiUser(val id: Long, val name: String, val email: String)

fun ApiUser.toDomain() = DomainUser(id, name)
fun DomainUser.toApi(email: String) = ApiUser(id, name, email)

// 15) Common patterns

// API DTOs
@@Serializable data class CreatePostRequest(val title: String, val body: String, val tags: List<String>)
@@Serializable data class CreatePostResponse(val id: Long, val createdAt: Long)

// Form state in Compose
data class LoginUiState(
    val email: String = "",
    val password: String = "",
    val busy: Boolean = false,
    val error: String? = null,
) {
    val isValid: Boolean get() = email.contains('@@') && password.length >= 8
}

// Domain events
sealed class CartEvent {
    data class ItemAdded(val sku: String, val qty: Int)    : CartEvent()
    data class ItemRemoved(val sku: String)                : CartEvent()
    object Cleared                                          : CartEvent()
}

// 16) Tips + bugs
//   • Don't put MUTABLE fields (var) in data classes if you'll use as map keys
//   • Equals depends on PRIMARY ctor params only — not body fields
//   • toString includes ALL primary ctor params — careful with secrets
//   • copy() is shallow — nested mutable types must be copied separately
//   • Use sealed + data for typed discriminated unions

Why it matters

Data classes replace 80% of POCOs. Free equals / hashCode / copy / destructuring — use them for DTOs, API responses, Compose UI state. Combine with sealed classes for type-safe discriminated unions.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
data class Point(val x: Int, val y: Int)
val p = Point(1, 2)
val p2 = p.copy(y = 5)   // free equals/hashCode/toString/copy
Try it Yourself »

Exercise

Generate equals/hashCode/copy automatically.

class Point(val x: Int, val y: Int)

Test yourself

Q1. Data classes auto-generate…
Q2. Copy with a few changes uses…
Q3. Data classes are best for…

Discussion

Loading…