Table of Contents
Understanding Data Classes in Kotlin
In many Android apps you will often need to store and pass around simple pieces of structured data, such as a user profile, a network response, or the state of a screen. Kotlin provides a special type of class called a data class that is designed exactly for this purpose.
A data class focuses on holding data rather than behavior. It saves you from writing a lot of repeated code that you would otherwise need in a normal class, such as methods to compare objects, create copies, or turn them into strings for logging.
To declare a data class you use the data keyword before class. The primary constructor parameters that represent the data must be marked as val or var so they become properties of the class.
data class User(
val id: Int,
val name: String,
val email: String
)This single line gives you a full featured class that you can use in your Android code.
Automatically Generated Functions
A normal class in Kotlin only has the functions that you explicitly write. A data class is different. The compiler automatically creates several useful functions for you based on the properties in the primary constructor.
It generates equals and hashCode so that two data class instances are considered equal if all their properties are equal. This is very convenient when you want to compare state objects, list items, or cache keys.
For example:
val user1 = User(1, "Alice", "alice@example.com")
val user2 = User(1, "Alice", "alice@example.com")
println(user1 == user2) // true
With an ordinary class, the same code would usually print false, because the default comparison would compare object references, not content. Data classes compare the actual data.
The generated hashCode function is consistent with equals. This is important when you use data class objects as keys in hash based collections such as HashMap or HashSet.
The compiler also generates a toString function that prints all properties in a readable format. This is very useful when you log objects while debugging Android apps.
println(user1.toString())
// Output: User(id=1, name=Alice, email=alice@example.com)
You also get a copy function. It creates a new instance with the same property values as an existing one, and lets you change only some of them. This is helpful when you treat your objects as mostly immutable and want to update one field without modifying the original.
val updatedUser = user1.copy(email = "alice@newmail.com")
// updatedUser.id is still 1
// updatedUser.name is still "Alice"
// updatedUser.email is "alice@newmail.com"
A Kotlin data class automatically provides equals, hashCode, toString, and copy based only on the properties in its primary constructor.
Requirements and Limitations of Data Classes
Data classes must satisfy a few rules. These rules keep the behavior predictable and avoid confusing or ambiguous cases.
The primary constructor must have at least one parameter. You cannot have an empty data class without any properties. All those parameters must be marked as val or var because they are the properties that define the data.
A data class cannot be abstract, open, sealed, or inner. You still can use data classes with other features of Kotlin, but the class itself must be concrete and cannot be used as an inner class.
You can add more properties in the body of a data class, but only the properties in the primary constructor are used for equals, hashCode, toString, and copy. This is something you must understand clearly when you design your models.
data class Session(
val token: String,
val userId: Int
) {
// Not part of equals, hashCode, or copy
var lastUsedTimestamp: Long = 0L
}
Two Session instances with the same token and userId are considered equal even if their lastUsedTimestamp values are different, because that property is not part of the primary constructor.
Destructuring Declarations
An extra convenience of data classes is destructuring. Each property of the primary constructor is mapped to a generated function named component1, component2, and so on. You can use these methods indirectly by destructuring an instance into separate variables.
val user = User(1, "Alice", "alice@example.com")
val (id, name, email) = user
println(id) // 1
println(name) // Alice
println(email) // alice@example.comDestructuring can make code more readable when you want to immediately work with individual fields, for example when you unpack a result from a repository or a pair of coordinates.
In many Android architectures you might return small state objects from a ViewModel. Destructuring can help unpack them when you only need certain parts.
Data Classes in Android Context
Even though the full Android usage of data classes is covered in other chapters, it is useful to see where they often appear. Data classes are typically used as models for UI state, API responses, database rows, item models for RecyclerView, and navigation arguments.
Because they provide content based equals, they work well with diffing utilities such as DiffUtil when updating lists. The copy function is convenient when you manage immutable UI state in patterns such as MVVM.
When you design data classes for Android, try to keep them focused on data. Avoid putting Android framework logic in these classes. Treat them more like plain containers, so they remain easy to test and reuse.
Understanding Enums in Kotlin
Enums represent a fixed set of constant values. They are useful whenever you have a variable that should only take one of a limited list of options, such as a status, a mode, or a type.
In Kotlin you declare an enum with the enum class keyword. Inside the body you list the possible values separated by commas.
enum class Priority {
LOW,
MEDIUM,
HIGH
}Each item you declare inside an enum is called an enum constant. You use these constants like normal values in your code.
val currentPriority: Priority = Priority.HIGHUsing enums instead of arbitrary strings or numbers helps prevent mistakes and makes the code more explicit and self documenting.
Enum Properties and Methods
An enum constant has some built in properties and methods that you get automatically.
Every constant has a name property which is the text you wrote for that constant, and an ordinal property which is its position in the list starting from zero.
val p = Priority.MEDIUM
println(p.name) // MEDIUM
println(p.ordinal) // 1
There is also a values() function that returns all constants in an array, and a valueOf(String) function that returns the constant with the matching name or throws an exception if it does not exist.
val allPriorities = Priority.values()
val high = Priority.valueOf("HIGH")You can use these functions when you need to loop through all options or convert from a stored string back to an enum.
Enums with Custom Properties and Constructors
Enums in Kotlin are more powerful than simple lists of constants. An enum can also have its own properties and a primary constructor. Each constant must then provide the appropriate arguments.
This is useful when each enum constant needs extra data, such as a human readable label, a numeric code, or a color value.
enum class Priority(val label: String, val level: Int) {
LOW("Low priority", 1),
MEDIUM("Medium priority", 2),
HIGH("High priority", 3)
}You can then access these properties on the constants.
val priority = Priority.HIGH
println(priority.label) // High priority
println(priority.level) // 3An enum can also have methods. If all constants share the same behavior you write the method in the body of the enum. If different constants need different implementations of the same method you can provide bodies for each constant individually.
enum class DownloadState {
NOT_STARTED,
IN_PROGRESS,
COMPLETED;
fun isActive(): Boolean {
return this == IN_PROGRESS
}
}Here all states share the same method implementation. You call it on any constant.
You can also write more advanced behavior.
enum class DownloadState {
NOT_STARTED {
override fun isTerminal() = false
},
IN_PROGRESS {
override fun isTerminal() = false
},
COMPLETED {
override fun isTerminal() = true
};
abstract fun isTerminal(): Boolean
}In this case each constant overrides the abstract method with its own result.
Using Enums in Control Flow
Enums fit naturally with Kotlin control flow, especially with the when expression. A when with an enum lets you handle each possible value explicitly.
fun describePriority(priority: Priority): String {
return when (priority) {
Priority.LOW -> "Not urgent"
Priority.MEDIUM -> "Important but not critical"
Priority.HIGH -> "Must handle immediately"
}
}
A when expression that covers all enum constants is exhaustive. This means you do not need an else branch, because the compiler knows all possible values. This helps you keep your code safe when you later add new enum constants. The compiler will alert you about places where the new constant is not handled.
Use when with enums and cover every constant. The compiler will then enforce exhaustiveness and help you avoid missing cases when the enum changes.
Enums in Android Context
Enums can represent clear states in Android apps, such as a network status, a screen mode, or a theme choice. They improve readability compared to loosely defined integers or strings.
For example you can track a network call state using an enum and expose it from a ViewModel to the UI. The UI can then react differently when the state is LOADING, SUCCESS, or ERROR.
Be aware that enums have some memory overhead compared to using integers or simple strings, especially if you create many instances or store them in large collections. For most beginner and medium sized apps the clarity and safety of enums is usually worth this cost.
Combining Data Classes and Enums
It is common to combine data classes and enums when modeling your app. A data class can include enum properties to express restricted states or categories, and enums can use data classes as part of more complex structures.
For instance you can represent a task in a to do app with a data class that uses an enum to describe priority.