In almost every Android app, you need model classes — a User, an Article, a Product. In Java, creating these means writing constructors, getters, setters, toString(), equals(), and hashCode() — all manually, all repetitive, all error-prone. Kotlin's data class generates all of this automatically in a single line. This guide covers everything about data classes with practical examples.


The Problem Data Classes Solve

Let's say you need a simple User model with three fields.

Java — 40+ lines:

public class User {
    private String name;
    private int age;
    private String email;

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getEmail() { return email; }

    public void setName(String name) { this.name = name; }
    public void setAge(int age) { this.age = age; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", email='" + email + "'}";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return age == user.age &&
               Objects.equals(name, user.name) &&
               Objects.equals(email, user.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, email);
    }
}

Kotlin data class — 1 line:

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

Same result. All of the above is generated automatically.


What Does data class Generate?

When you declare a data class, Kotlin automatically generates:

Generated What It Does
Constructor Creates objects with all properties
toString() Readable string representation
equals() Compares by content, not reference
hashCode() Consistent with equals()
copy() Creates a modified copy
componentN() Enables destructuring
data class User(val name: String, val age: Int, val email: String)

val user = User("Alice", 25, "alice@email.com")

// toString() — auto-generated
println(user)
// User(name=Alice, age=25, email=alice@email.com)

// equals() — compares by content
val user2 = User("Alice", 25, "alice@email.com")
println(user == user2)   // true — same content
println(user === user2)  // false — different objects in memory

// copy() — create modified version
val updatedUser = user.copy(age = 26)
println(updatedUser)
// User(name=Alice, age=26, email=alice@email.com)
println(user)            // original unchanged
// User(name=Alice, age=25, email=alice@email.com)

// Destructuring — componentN()
val (name, age, email) = user
println("$name is $age years old")  // Alice is 25 years old

copy() — Creating Modified Copies

copy() is one of the most useful features of data classes. It creates a new object with the same values, except for the ones you explicitly change.

data class Article(
    val id: Int,
    val title: String,
    val content: String,
    val isPublished: Boolean = false,
    val viewCount: Int = 0,
    val tags: List<String> = emptyList()
)

val draft = Article(
    id = 1,
    title = "Kotlin Data Classes",
    content = "Everything you need to know..."
)

// Publish the article — only change isPublished
val published = draft.copy(isPublished = true)

// Update view count — only change viewCount
val viewed = published.copy(viewCount = published.viewCount + 1)

// Add tags — only change tags
val tagged = viewed.copy(tags = listOf("Kotlin", "Android", "Basics"))

println(draft.isPublished)     // false — original unchanged
println(published.isPublished) // true
println(tagged.tags)           // [Kotlin, Android, Basics]

Why copy() matters — immutability:

Data classes with val properties are immutable — you can't change them after creation. Instead of modifying an object, you create a new one with copy(). This is the foundation of safe, predictable code, especially in ViewModels and state management.

// Instead of mutating state
uiState.isLoading = true   // ❌ dangerous if state is shared

// Create a new state with copy
_uiState.value = _uiState.value.copy(isLoading = true)  // ✅ safe

equals() and hashCode() — Content Comparison

By default, Kotlin (and Java) objects are compared by reference — two variables are equal only if they point to the exact same object in memory. Data classes override this to compare by content instead.

// Regular class — reference comparison
class PointRegular(val x: Int, val y: Int)

val p1 = PointRegular(1, 2)
val p2 = PointRegular(1, 2)
println(p1 == p2)   // false — different objects in memory

// Data class — content comparison
data class Point(val x: Int, val y: Int)

val p3 = Point(1, 2)
val p4 = Point(1, 2)
println(p3 == p4)   // true — same content

This matters a lot when:

  • Checking if a user is already in a list
  • Comparing API responses with cached data
  • Using objects as map keys
  • Detecting changes in LiveData / StateFlow
data class User(val id: String, val name: String)

val users = setOf(User("1", "Alice"), User("2", "Bob"))
println(User("1", "Alice") in users)   // true — data class finds it by content

Destructuring

Data classes automatically support destructuring — unpacking an object's properties into individual variables.

data class Coordinate(val lat: Double, val lng: Double)
data class Article(val id: Int, val title: String, val author: String)

val location = Coordinate(28.6139, 77.2090)
val (lat, lng) = location
println("Lat: $lat, Lng: $lng")   // Lat: 28.6139, Lng: 77.209

val article = Article(1, "Kotlin Guide", "John")
val (id, title, author) = article
println("$id: $title by $author")   // 1: Kotlin Guide by John

Destructuring in loops:

val articles = listOf(
    Article(1, "Kotlin Basics", "Alice"),
    Article(2, "Coroutines", "Bob"),
    Article(3, "Jetpack Compose", "Charlie")
)

for ((id, title, author) in articles) {
    println("[$id] $title — $author")
}
// [1] Kotlin Basics — Alice
// [2] Coroutines — Bob
// [3] Jetpack Compose — Charlie

Skip a component with _:

val article = Article(1, "Kotlin Guide", "John")
val (_, title, _) = article   // only care about title
println(title)   // Kotlin Guide

Data Class Requirements and Limitations

Requirements

  • Must have at least one property in the primary constructor
  • Properties in the primary constructor must be val or var

Limitations

  • Cannot be abstract, open, sealed, or inner
  • Can extend other classes and implement interfaces (since Kotlin 1.1)
interface Displayable {
    fun display(): String
}

abstract class Entity(val id: Int)

// Data class extending class and implementing interface
data class Product(
    val productId: Int,
    val name: String,
    val price: Double
) : Entity(productId), Displayable {
    override fun display() = "$name — $$price"
}

Properties Outside the Primary Constructor

Only properties declared in the primary constructor are used in equals(), hashCode(), toString(), and copy(). Properties defined in the class body are ignored by these generated functions.

data class User(val name: String, val email: String) {
    var isLoggedIn: Boolean = false   // NOT included in equals/hashCode/toString
    val nameLength: Int get() = name.length  // NOT included either
}

val u1 = User("Alice", "alice@email.com")
val u2 = User("Alice", "alice@email.com")
u1.isLoggedIn = true

println(u1 == u2)   // true — isLoggedIn is ignored in equals()
println(u1)         // User(name=Alice, email=alice@email.com) — isLoggedIn not shown

This is intentional — keep your data class constructor properties to the core identity of the object.


Nested Data Classes

Data classes can contain other data classes:

data class Address(
    val street: String,
    val city: String,
    val country: String,
    val zipCode: String
)

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

val user = User(
    id = "u001",
    name = "Alice",
    email = "alice@email.com",
    address = Address(
        street = "123 Main St",
        city = "New York",
        country = "USA",
        zipCode = "10001"
    )
)

println(user)
// User(id=u001, name=Alice, email=alice@email.com,
//      address=Address(street=123 Main St, city=New York, country=USA, zipCode=10001))

// Update just the city using nested copy
val movedUser = user.copy(
    address = user.address.copy(city = "Los Angeles", zipCode = "90001")
)
println(movedUser.address.city)   // Los Angeles
println(user.address.city)        // New York — original unchanged

Data Classes for API Responses

This is one of the most common uses — modeling JSON responses with data classes:

data class ApiResponse<T>(
    val success: Boolean,
    val data: T?,
    val message: String?,
    val errorCode: Int? = null
)

data class ArticleResponse(
    val id: Int,
    val title: String,
    val content: String,
    val author: AuthorResponse,
    val publishedAt: String,
    val tags: List<String>,
    val viewCount: Int
)

data class AuthorResponse(
    val id: Int,
    val name: String,
    val avatarUrl: String?
)

// With Gson/Moshi — maps directly to/from JSON
// {
//   "id": 1,
//   "title": "Kotlin Data Classes",
//   "content": "...",
//   "author": { "id": 5, "name": "John", "avatarUrl": null },
//   "publishedAt": "2026-01-15",
//   "tags": ["Kotlin", "Android"],
//   "viewCount": 1250
// }

Data Classes for UI State

A powerful pattern in modern Android — using data classes to represent the complete UI state:

data class ArticleListUiState(
    val isLoading: Boolean = false,
    val articles: List<Article> = emptyList(),
    val errorMessage: String? = null,
    val selectedCategory: String = "All",
    val searchQuery: String = "",
    val hasMorePages: Boolean = true
)

class ArticleViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(ArticleListUiState())
    val uiState: StateFlow<ArticleListUiState> = _uiState

    fun loadArticles() {
        // Update only isLoading — copy() keeps everything else the same
        _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)

        viewModelScope.launch {
            try {
                val articles = repository.getArticles()
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    articles = articles
                )
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    errorMessage = e.message
                )
            }
        }
    }

    fun selectCategory(category: String) {
        _uiState.value = _uiState.value.copy(selectedCategory = category)
        loadArticles()
    }

    fun updateSearch(query: String) {
        _uiState.value = _uiState.value.copy(searchQuery = query)
    }
}

This pattern — a single data class representing all UI state, updated with copy() — is used in almost every modern Android app.


data class vs Regular class — When to Use Which

Use data class when Use regular class when
Modeling data (API response, DB entity, UI state) Modeling behavior (Repository, ViewModel, Manager)
Need equals() by content Need equals() by reference (default)
Need copy() Need inheritance with open
Need toString() for debugging Have complex logic and lifecycle
Simple value holders Android components (Activity, Fragment, Service)
// ✅ Use data class
data class Article(val id: Int, val title: String)
data class LoginUiState(val isLoading: Boolean, val error: String?)
data class UserPreferences(val theme: String, val language: String)

// ✅ Use regular class
class ArticleRepository(private val api: ApiService)
class LoginViewModel(private val authService: AuthService) : ViewModel()
class ImageLoader(private val context: Context)

Common Mistakes to Avoid

Mistake 1: Using var when val is sufficient

// ❌ Mutable data class — loses immutability benefits
data class User(var name: String, var age: Int)

// ✅ Prefer val — use copy() to "change" values
data class User(val name: String, val age: Int)
val updated = user.copy(age = 26)

Mistake 2: Putting business logic in data classes

// ❌ Data class doing too much
data class User(val name: String, val email: String) {
    fun sendWelcomeEmail() { /* ... */ }
    fun validateEmail(): Boolean { /* ... */ }
    fun fetchProfilePicture() { /* ... */ }
}

// ✅ Keep data class simple — put logic in a repository or use case
data class User(val name: String, val email: String)

class UserService {
    fun sendWelcomeEmail(user: User) { /* ... */ }
    fun validateEmail(email: String): Boolean { /* ... */ }
}

Mistake 3: Forgetting that body properties are excluded from equals()

data class Product(val id: Int, val name: String) {
    var isInCart: Boolean = false  // excluded from equals!
}

val p1 = Product(1, "Phone")
val p2 = Product(1, "Phone")
p1.isInCart = true

println(p1 == p2)  // true — isInCart is ignored
// This might be surprising — make sure it's what you want

Mistake 4: Empty data class

// ❌ Data class with no constructor properties
data class Empty()   // pointless — use object instead

// ✅ Use object for singletons with no data
object EmptyState

Summary

  • data class automatically generates toString(), equals(), hashCode(), copy(), and componentN()
  • Use copy() to create modified versions of an object — the foundation of immutable state management
  • equals() compares by content — two data class instances with the same values are equal
  • Only constructor properties are included in generated functions — body properties are excluded
  • Destructuring lets you unpack data class properties into variables with val (a, b, c) = object
  • Use data classes for models, API responses, database entities, and UI state
  • Keep data classes simple — just data, minimal behavior
  • Prefer val over var in data classes to maintain immutability

Data classes are one of the most used features in Kotlin Android development. Once you understand copy() with StateFlow, you'll see this pattern in virtually every modern Android codebase.

Happy coding!