0Pricing
Android Academy · Lesson

Data Classes & Sealed Classes

Use data classes for value-based equality and copy(), and sealed classes for exhaustive type hierarchies and clean state modeling.

Data Classes & Sealed Classes is a free Android Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Are Data Classes?

A data class in Kotlin is a class whose primary purpose is to hold data. The compiler automatically generates useful methods for you:

  • equals() — compare two instances by value
  • hashCode() — consistent hash based on properties
  • toString() — readable string representation
  • copy() — create a modified copy
  • Destructuring support

Declaring a Data Class

Add the data keyword before class. All properties must be in the primary constructor:

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

fun main() {
    val u1 = User(1, "Alice", "alice@example.com")
    val u2 = User(1, "Alice", "alice@example.com")

    println(u1)          // User(id=1, name=Alice, email=alice@example.com)
    println(u1 == u2)    // true  (value equality)
    println(u1 === u2)   // false (different objects)
}

copy() — Modify One Field

copy() creates a new instance with some properties changed. The original is not modified (immutability):

data class Product(val name: String, val price: Double, val inStock: Boolean)

fun main() {
    val original = Product("Laptop", 999.0, true)

    // Only change the price; keep everything else
    val onSale = original.copy(price = 799.0)

    println(original)  // Product(name=Laptop, price=999.0, inStock=true)
    println(onSale)    // Product(name=Laptop, price=799.0, inStock=true)
}

Destructuring Declarations

Data classes support destructuring — unpack properties into variables using componentN() functions generated by the compiler:

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

fun main() {
    val point = Point(10, 20)

    // Destructure into separate variables
    val (x, y) = point
    println("x=$x, y=$y")   // x=10, y=20

    // Also works in for loops with maps
    val scores = mapOf("Alice" to 95, "Bob" to 88)
    for ((name, score) in scores) {
        println("$name: $score")
    }
}

What Are Sealed Classes?

A sealed class is a restricted class hierarchy — all subclasses must be defined in the same file. This makes the set of possible types known at compile time.

Use sealed classes to represent a finite set of states, like network results, UI states, or navigation events.

Sealed Class Syntax

Declare subclasses directly inside or in the same file:

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

fun handle(result: NetworkResult<String>) {
    when (result) {
        is NetworkResult.Success -> println("Data: ${result.data}")
        is NetworkResult.Error  -> println("Error ${result.code}: ${result.message}")
        NetworkResult.Loading   -> println("Loading...")
    }
}

Sealed Class + when (Exhaustive)

The power of sealed classes: when is exhaustive. The compiler forces you to handle every subtype — no forgotten cases:

sealed class UiState {
    object Loading : UiState()
    data class Success(val message: String) : UiState()
    data class Error(val error: String) : UiState()
}

fun render(state: UiState): String = when (state) {
    is UiState.Loading      -> "Showing spinner"
    is UiState.Success      -> "Show: ${state.message}"
    is UiState.Error        -> "Error: ${state.error}"
    // Compiler error if you remove any branch!
}

fun main() {
    println(render(UiState.Loading))
    println(render(UiState.Success("Loaded!")))
}

Sealed Class vs Enum

Both represent a fixed set of values, but:

  • Enum — each constant is a single instance, no extra data per variant
  • Sealed class — each variant can carry different data, be a class or object

Use enum when variants need no data. Use sealed class when variants need different properties.

Object Declarations (Singletons)

object creates a singleton — one instance, created lazily, thread-safe:

object AppConfig {
    const val BASE_URL = "https://api.example.com"
    const val TIMEOUT = 30L

    fun buildUrl(path: String) = "$BASE_URL/$path"
}

fun main() {
    println(AppConfig.BASE_URL)          // https://api.example.com
    println(AppConfig.buildUrl("users")) // https://api.example.com/users
}

Companion Objects

A companion object lives inside a class and provides class-level members (like Java's static). Commonly used for factory methods:

class Token private constructor(val value: String) {
    companion object {
        fun create(raw: String): Token {
            val cleaned = raw.trim()
            require(cleaned.isNotEmpty()) { "Token must not be blank" }
            return Token(cleaned)
        }
    }
}

fun main() {
    val token = Token.create("  abc123  ")
    println(token.value)  // abc123
}

Data Classes in a Real App

Combine data classes with sealed classes for clean state modeling:

data class Post(val id: Int, val title: String, val body: String)

sealed class PostState {
    object Loading : PostState()
    data class Loaded(val posts: List<Post>) : PostState()
    data class Failed(val reason: String) : PostState()
}

fun showState(state: PostState) = when (state) {
    PostState.Loading        -> println("Fetching posts...")
    is PostState.Loaded      -> println("${state.posts.size} posts loaded")
    is PostState.Failed      -> println("Failed: ${state.reason}")
}

fun main() {
    showState(PostState.Loaded(listOf(
        Post(1, "Hello", "World"),
        Post(2, "Kotlin", "Rocks")
    )))
}

Quick Check

Which statement best describes what makes sealed classes different from regular abstract classes?

Recap: Data & Sealed Classes

You now have two powerful Kotlin tools:

  • data class — value-based equality, copy(), destructuring, auto-generated toString()
  • sealed class — finite type hierarchy, exhaustive when, each variant can carry its own data
  • object — singleton, companion objects for factory methods

Together, these patterns replace Java boilerplate and enable clean, safe state modeling.

Frequently asked questions

Is the “Data Classes & Sealed Classes” lesson free?

Yes — the full text of “Data Classes & Sealed Classes” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “Data Classes & Sealed Classes”?

Use data classes for value-based equality and copy(), and sealed classes for exhaustive type hierarchies and clean state modeling. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Data Classes & Sealed Classes” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Null Safety
  2. Classes & Objects
  3. Data Classes & Sealed Classes
  4. Extension Functions & Higher-Order Functions
← Back to Android Academy