A data class automatically generates equals(), hashCode(), toString(), and a copy() method — ideal for simple models:

data class User(val id: Int, val name: String, val email: String)

val user1 = User(1, "Ada", "ada@example.com")
val user2 = user1.copy(name = "Ada Lovelace")  // copies with one field changed
println(user1 == user2)   // false — structural equality, comparing all fields

Sealed classes

A sealed class restricts a type hierarchy to a known, closed set of subclasses — ideal for representing UI state or a network result, where you want the compiler to force you to handle every case:

sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val message: String) : NetworkResult<Nothing>()
    object Loading : NetworkResult<Nothing>()
}

fun handleResult(result: NetworkResult<List<User>>) {
    when (result) {
        is NetworkResult.Success -> showUsers(result.data)
        is NetworkResult.Error -> showError(result.message)
        NetworkResult.Loading -> showSpinner()
    }   // no 'else' needed — the compiler knows these are ALL the possible cases
}

This pattern will come back constantly once you get to networking and ViewModel state later in this course.