The Ultimate Kotlin Class Cheat Sheet: Mastering Every Class Type (from Data to Sealed)

 

From Data to Sealed: Crucial Distinctions, Hidden Caveats (Inner Class Leaks & Value Class Boxing), and Modern Best Practices.


The Ultimate Kotlin Class Cheat Sheet: Mastering Every Class Type (from Data to Sealed)

Kotlin is a modern language, and one of its greatest strengths is offering specialized class types to solve specific programming problems elegantly. If you’ve ever felt overwhelmed by choices like datasealedinner, and value, you’re not alone.

This guide distills the purpose of every major Kotlin class type, providing enhanced explanations, crucial constraints, and practical, real-world examples to help you choose the right tool for the job.

1. The Building Blocks: Regular, Nested, and Local Classes

These are the foundation of object-oriented programming in Kotlin.

class (Regular Class)

This is your default choice when you need an object to hold state AND define behavior.

// A regular class defines both data (properties) and actions (methods).
class ServiceManager(private val logFile: String) {
var isRunning: Boolean = false
private set // State property

fun startService() {
if (!isRunning) {
println("Starting service...")
// Logic to initiate service...
isRunning = true
}
}

// Behavior property
fun logStatus() = println("Current log file: $logFile")
}

Nested and Local Classes

These classes are defined by their scope:

Press enter or click to view image in full size
Nested and Local Classes




2. The Relationship Keeper: inner class

An inner class is a nested class that holds a strong, implicit reference to its outer class instance. This allows it to access the outer class’s members, including private properties.

Use Case: Building a component that is fundamentally tied to — and dependent on — the state of its parent.

class AppSettings(val appVersion: String) {
private val debugMode: Boolean = true // Private state

// An inner class needs a reference to its parent (AppSettings)
// to access members like 'debugMode'.
inner class ConfigurationReader {
fun getReport() {
// Can access 'debugMode' and 'appVersion' from AppSettings.
if (debugMode) {
println("CONFIG: Running in Debug Mode for v$appVersion.")
} else {
println("CONFIG: Production ready.")
}
}
}
}

⚠️ Critical Warning: Memory Leaks Use inner class sparingly — especially in environments like Android. Because the inner class keeps a strong reference to its outer class instance, it can easily prevent the outer object from being garbage collected, leading to a memory leak if the inner object outlives the outer one. Prefer passing required dependencies explicitly instead.

3. The Data Dynamo: data class

If your class is primarily a container for data, use data class. The Kotlin compiler automatically generates powerful, boilerplate-reducing functions.

Key Auto-Generated Functions:

  1. equals() and hashCode(): Compares objects based on the values of the properties in the primary constructor (structural equality).
  2. toString(): Prints a readable string representation of all properties.
  3. copy(): Creates an identical copy of the object, allowing for modification of specific properties without mutating the original (crucial for immutable programming).
  4. componentN(): Supports destructuring.
// This class is used purely to hold inventory information.
data class InventoryItem(
val sku: String,
val name: String,
val quantity: Int // Must have at least one property
)

fun processItem() {
val original = InventoryItem("A901", "Coffee Mug", 50)

// Destructuring: Extract properties easily
val (code, itemName, stock) = original

// Copying: Create a new object with one change
val updated = original.copy(quantity = 65)

println(original == updated) // Output: false (structural comparison)
}

🛑 Data Class Limitations Data classes are best suited for immutable data holders and have several restrictions:

  • They require at least one parameter in the primary constructor marked as val or var.
  • They cannot be declared abstractopensealed, or inner.
  • They cannot directly inherit from another data class.

4. The Performance Optimizer: value class (Inline)

A value class allows you to wrap a simple, single value (like a String or Int) in a stronger type without necessarily incurring the runtime overhead of creating an actual object instance on the heap.

Goal: Achieve type safety while minimizing memory allocations.

// Requires the JvmInline annotation for JVM projects
@JvmInline
value class AccessToken(val value: String)

// If we used a regular String, we could accidentally swap two
// String parameters in a function call. This prevents that.
fun fetchData(token: AccessToken, endpoint: String) {
// ... use token.value to make API call
println("Fetching data for $endpoint using token: ${token.value.substring(0, 5)}...")
}

💡 Precision: The Boxing Caveat Value classes may be inlined by the compiler to use the primitive type directly, but boxing (wrapping the value back in an object) can still occur, depending on usage. Boxing is triggered when the value class is:

  1. Used as a generic type argument (e.g., List<AccessToken>).
  2. Treated as a nullable type (e.g., AccessToken?).
  3. Used as a property of a Sealed or Enum class.

While inlining is not guaranteed in all cases, the primary benefit is always the strong type safety it provides over a simple typealias.

5. The Definitive Sets: enum vs. sealed

These are the most powerful types for dealing with a constrained set of possibilities, enabling the compiler to check for completeness.

enum class (Fixed Instances)

Use when you need a fixed set of objects (instances). Each instance of an Enum is known at compile time and is a unique object.

// Represents a fixed, finite set of states for a traffic light.
enum class TrafficLightStatus(val durationSeconds: Int) {
RED(30), // Instances can have properties
YELLOW(5),
GREEN(45);

fun isStopping() = this == RED || this == YELLOW
}

sealed class and sealed interface (Fixed Subtypes)

Use when you need a fixed set of subtypes (classes or interfaces) that might carry different kinds of data.

// Sealed Interface is often preferred for more flexible hierarchies (new best practice).
sealed interface Result<out T> {
// Subtypes must be defined in the same module/package
data class Success<T>(val data: T) : Result<T> // Subtype 1
class Error(val errorCode: Int, val message: String) : Result<Nothing> // Subtype 2
object Loading : Result<Nothing> // Subtype 3 (uses Object for a stateless singleton)
}

fun handleResult(response: Result<String>) {
// Kotlin guarantees all direct subtypes are covered (exhaustive 'when')
when (response) {
is Result.Success -> println("Data received: ${response.data}")
is Result.Error -> println("Error ${response.errorCode}: ${response.message}")
Result.Loading -> println("Request in progress...")
}
}

🌟 Modern Kotlin Best Practice Sealed Interfaces are often preferred over sealed classes when modeling states. They offer the same exhaustive checking in when expressions but allow the subtypes to inherit from other classes (since a class can implement multiple interfaces), giving you greater design flexibility.

6. Your Essential Addition: object Declaration

The object declaration is the idiomatic Kotlin way to create a Singleton—a class with only one instance.

Use Case: Utility classes, logging handlers, or anything that should only exist once in the application’s lifetime.

// 'object' declares a class and creates a single instance of it immediately.
object AppLogger {
private var logCount = 0

fun log(message: String) {
logCount++
// Log to a file or remote service
println("[LOG #$logCount] $message")
}
}

fun initApplication() {
AppLogger.log("Application started.") // Called directly on the object name
}

Initialization Detail: An object declaration is initialized lazily—only when it is accessed for the first time—and this initialization is thread-safe by default.

7. The Framework Tool: annotation class

These are used to create custom metadata tags that you can attach to code elements.

// Defining custom annotations requires the 'annotation' modifier.
annotation class Important(val priority: Int = 1)

// Meta-annotations define how and where the annotation can be used:
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExperimentalFeature

Frequently Asked Questions (FAQs)

If I’m just storing data, why not always use data class?

data class is great for simple data carriers (DTOs), but it shouldn’t be used if the object’s primary role is to define complex behavior, handle dependencies, or when you need reference equality (checking if two variables point to the exact same object in memory). Use a regular class for complex domain models and services.

What exactly does “Exhaustive when” mean for Sealed and Enum classes?

It means the Kotlin compiler forces you to handle every possible case (every Enum constant or every Sealed subtype) inside a when expression. If you forget one, the code won’t compile. This eliminates a huge source of runtime bugs, especially when you add a new constant/subtype later.

Is a value class the same as a typealias?

No. A typealias is just an alternative name for an existing type (e.g., typealias ID = String), offering zero type safety (you can still pass any String where an ID is expected). A value class creates a distinct, new type while still compiling down to the performance equivalent of its underlying type (unless boxed), giving you both safety and speed.

Wrapping Up

Kotlin’s class system is not about complexity; it’s about precision. By choosing the right class type — whether it’s a data class for a simple DTO, a sealed class for state management, or a value class for safer APIs—you make your code more expressive, safer, and often more performant.

What are your thoughts?

  • How has the memory leak warning for inner class impacted your design decisions, especially in mobile development?
  • Have you already switched from using sealed class to sealed interface for modeling your network results? Share why or why not!

If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏

Comments

Popular posts from this blog

Stop Writing Massive when Statements: Master the State Pattern in Kotlin

Coroutines & Flows: 5 Critical Anti-Patterns That Are Secretly Slowing Down Your Android App

Code Generation vs. Reflection: A Build-Time Reliability Analysis