Kotlin enum classes elevate constants into powerful, type-safe objects with constructors, properties, and functions, far beyond simple value lists. π Whether defining fixed sets like colors or building complex state machines, enums offer unmatched flexibility and safety. This 2025 guide dives deep into every aspect of enum classes, with pro-level tips, advanced use cases, and real-world examples to make your code shine! π
Basic Enum Classes π
A basic enum class defines a fixed set of constants, each an instance of the class, offering type-safe, concise categorization. π
Provided Example: Simple enum:
enum class Color { // π Basic enum
RED, GREEN, BLUE // π Constants
}
fun main() {
val color = Color.RED // ✅ Select constant
println(color) // Outputs: RED π
}
Enum Basics:
- π Fixed Set: Defines a limited, type-safe set of constants. ✅
- ⚡ Object Instances: Each constant is a singleton instance of the enum class. π
- π Simple Syntax: Comma-separated constants, no extra baggage. π§Ή
- ✅ Use Case: Categorizing states, types, or options (e.g., colors, days). π§
Basic Tip: Use simple enums for lightweight, constant-based categorization without additional logic. π
Enums with Constructors π§
Enum classes can include constructors to attach data to each constant, making them powerful data holders. π¨
Provided Example: Enum with constructor:
enum class Color(val rgb: Int) { // π§ Constructor with RGB
RED(0xFF0000), // π Red constant
GREEN(0x00FF00), // π Green constant
BLUE(0x0000FF) // π Blue constant
}
fun main() {
val color = Color.RED // ✅ Select constant
println("RGB: ${color.rgb.toString(16)}") // Outputs: RGB: ff0000 π
}
Constructor Benefits:
- π¦ Data Storage: Each constant holds custom data via constructor parameters. ✅
- ⚡ Expressive: Enhances enums with meaningful attributes (e.g., RGB values). π
- π Type-Safe: Ensures constants are instantiated with valid data. π‘️
- ✅ Use Case: Attach metadata like codes, values, or labels to constants. π§
Constructor Tip: Use constructors to enrich enums with data, but keep parameters minimal for clarity. π
Functions and Properties in Enums π ️
Enums can define properties and functions, separated by a semicolon (`;`) after constants. Abstract members require each constant to provide an implementation, enabling complex logic. π§©
Provided Example: Enum with abstract property and function:
enum class Color { // π Enum with members
RED { // π Red constant
override val rgb: Int = 0xFF0000 // ✅ Override
},
GREEN { // π Green constant
override val rgb: Int = 0x00FF00 // ✅ Override
},
BLUE { // π Blue constant
override val rgb: Int = 0x0000FF // ✅ Override
};
abstract val rgb: Int // π§ Abstract property
fun colorString() = "#%06X".format(0xFFFFFF and rgb) // π Hex string maker
}
fun main() {
println(Color.RED.colorString()) // Outputs: #FF0000 π
}
Example: Enum with Concrete Property
enum class Color(val rgb: Int) { // π§ Constructor
RED(0xFF0000), // π Red
GREEN(0x00FF00), // π Green
BLUE(0x0000FF); // π Blue
val hex: String = "#%06X".format(0xFFFFFF and rgb) // π Concrete property
fun isBright(): Boolean = rgb and 0xFFFFFF > 0x7FFFFF // π Brightness check
}
fun main() {
val color = Color.GREEN // ✅ Select constant
println("${color.hex}, Bright: ${color.isBright()}") // Outputs: #00FF00, Bright: true π
}
Member Magic:
- π§° Rich Behavior: Add properties and methods like regular classes. ✅
- π Semicolon Separator: Required after constants if members are defined. π«
- π§ Abstract Members: Force constants to implement specific logic. π
- ✅ Use Case: Add computed properties or behavior to constants (e.g., hex formatting). π§
Member Tip: Use abstract members for per-constant customization; use concrete members for shared logic. π
Mutability in Enums π
Enums can have mutable properties, acting like singletons with changeable state, ideal for dynamic data. π¦
Provided Example: Mutable enum:
enum class Planet(var population: Long = 0) { // π§ Change to Long to support large values
EARTH(7_000_000_000), // π Earth
MARS; // π Mars
override fun toString() = "$name[population=$population]" // π Custom toString
}
fun main() {
println(Planet.MARS) // Outputs: MARS[population=0] π
Planet.MARS.population = 3 // π Update population
println(Planet.MARS) // Outputs: MARS[population=3] π
}
Mutable Might:
- π Dynamic State: Change properties with var. ✅
- π¦ Singleton Behavior: Each constant maintains its own state. π
- ⚡ Use Case: Track changing data like counters or statuses. π§
- π« Caveat: Mutable state can reduce predictability; use cautiously. π‘️
Mutability Tip: Limit mutability to specific use cases and document state changes to maintain clarity. π
Enum with Companion Object π§©
A companion object adds shared utilities or factory methods to an enum, enhancing its functionality. π
Provided Example: Enum with companion object:
enum class Color(val rgb: Int) { // π§ Constructor
RED(0xFF0000), // π Red
GREEN(0x00FF00), // π Green
BLUE(0x0000FF); // π Blue
companion object { // π§© Shared utilities
fun fromRgb(rgb: Int) = values().find { it.rgb == rgb } ?: BLUE // π Find by RGB
}
}
fun main() {
println(Color.fromRgb(0xFF0000)) // Outputs: RED π
println(Color.fromRgb(0xFFFFFF)) // Outputs: BLUE (default) π
}
Example: Companion with Factory Method
enum class Status(val code: Int) { // π§ Constructor
SUCCESS(200), // ✅ Success
ERROR(500), // π« Error
NOT_FOUND(404); // π Not found
companion object { // π§© Companion
fun fromCode(code: Int): Status? = values().find { it.code == code } // π Nullable find
fun allCodes(): List<Int> = values().map { it.code } // π List all codes
}
}
fun main() {
println(Status.fromCode(200)) // Outputs: SUCCESS π
println(Status.fromCode(999)) // Outputs: null π«
println(Status.allCodes()) // Outputs: [200, 500, 404] π
}
Companion Tricks:
- π§© Shared Logic: Centralize utility methods or factories. ✅
- π Lookups: Implement custom searches (e.g., by code or value). π
- ⚡ Enhanced Usability: Simplifies enum interaction with static-like methods. π§
- ✅ Use Case: Parsing, validation, or enum metadata access. π
Companion Tip: Use companion objects for lookup methods or shared utilities to keep enum logic clean and reusable. π
Using Enums in When Expressions π€
Enums pair seamlessly with when expressions for concise, type-safe logic, often requiring no else branch due to exhaustiveness. ⚖️
Provided Example: Enum in when:
enum class Color { // π Simple enum
RED, GREEN, BLUE // π Constants
}
fun main() {
val color = Color.RED // ✅ Select constant
val message = when (color) { // π€ Match enum
Color.RED -> "Hot!" // π₯
Color.GREEN -> "Cool!" // π₯Ά
Color.BLUE -> "Calm!" // π
}
println(message) // Outputs: Hot! π
}
Example: When with Constructor Data
enum class Color(val rgb: Int) { // π§ Constructor
RED(0xFF0000), // π Red
GREEN(0x00FF00), // π Green
BLUE(0x0000FF) // π Blue
}
fun main() {
val color = Color.GREEN // ✅ Select constant
val description = when (color) { // π€ Match with data
Color.RED -> "Red: #${color.rgb.toString(16)}" // π₯
Color.GREEN -> "Green: #${color.rgb.toString(16)}" // π₯Ά
Color.BLUE -> "Blue: #${color.rgb.toString(16)}" // π
}
println(description) // Outputs: Green: #00ff00 π
}
When Wins:
- π€ Clean Matching: Simplifies logic with direct enum constant checks. ✅
- ⚡ Exhaustive: No else needed for complete enum coverage. π§Ή
- π Data Access: Leverage constructor data in when branches. π
- ✅ Use Case: State machines, UI styling, or response handling. π§
When Tip: Use when with enums for readable, exhaustive logic; add an else only for incomplete or future-proofed enums. π
Advanced Use Case: State Machine with Enums πΉ️
Enums are perfect for modeling state machines, with each constant representing a state and methods defining transitions or actions. π§©
Example: Traffic Light State Machine
enum class TrafficLight(val duration: Int) { // π§ Constructor
RED(30) { // π Red state
override fun next() = GREEN // ✅ Transition
override fun action() = "Stop"
},
GREEN(30) { // π Green state
override fun next() = YELLOW // ✅ Transition
override fun action() = "Go"
},
YELLOW(5) { // π Yellow state
override fun next() = RED // ✅ Transition
override fun action() = "Caution"
};
abstract fun next(): TrafficLight // π Next state
abstract fun action(): String // π State action
}
fun main() {
var light = TrafficLight.RED // ✅ Initial state
repeat(3) { // π Simulate transitions
println("${light.action()} for ${light.duration}s") // π Print action
light = light.next() // π Move to next state
}
// Outputs: Stop for 30s, Go for 30s, Caution for 5s π
}
State Machine Benefits:
- πΉ️ State Modeling: Each constant represents a state with data and behavior. ✅
- π Transitions: Methods define state changes, ensuring type safety. π
- ⚡ Use Case: Traffic lights, workflows, or game states. π§
- π‘️ Safe: Enums enforce valid states and transitions. π«
State Machine Tip: Use enums for finite state machines with clear transitions; consider sealed classes for more complex hierarchies. π
Enums vs. Sealed Classes ⚖️
Enums and sealed classes both provide type-safe hierarchies, but they serve different purposes. Understanding their differences helps choose the right tool. π
Example: Enum vs. Sealed Class
enum class Result { // π Enum
SUCCESS, ERROR, LOADING // π Constants
}
sealed class ResultState { // π§© Sealed class
object Success : ResultState() // ✅ Success
data class Error(val message: String) : ResultState() // π« Error
object Loading : ResultState() // ⏳ Loading
}
fun main() {
val enumResult = Result.SUCCESS // ✅ Enum
val sealedResult = ResultState.Error("Failed") // ✅ Sealed
println(when (enumResult) { // π€ Enum when
Result.SUCCESS -> "Success!"
Result.ERROR -> "Error!"
Result.LOADING -> "Loading..."
}) // Outputs: Success! π
println(when (sealedResult) { // π€ Sealed when
is ResultState.Success -> "Success!"
is ResultState.Error -> "Error: ${sealedResult.message}"
is ResultState.Loading -> "Loading..."
}) // Outputs: Error: Failed π
}
Comparison:
- π Enums: Fixed set of singleton constants, ideal for simple, uniform states. ✅
- π§© Sealed Classes: Hierarchical, can have data classes or objects, ideal for complex, varied states. π
- ⚡ Performance: Enums are lighter; sealed classes offer more flexibility. π§
- ✅ Use Case: Use enums for fixed, similar states (e.g., colors); sealed classes for diverse states with data (e.g., API results). ⚖️
Comparison Tip: Choose enums for simple, constant-based states; use sealed classes for complex, data-rich hierarchies. π
Thread Safety in Enums π§΅
Enums are inherently thread-safe for their constant instances, but mutable properties require synchronization. π
Example: Thread-Safe Mutable Enum
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch
enum class Counter { // π Enum with mutable state
INSTANCE; // π Singleton constant
private var count: Int = 0 // π Mutable state
private val lock = Any() // π Synchronization lock
fun increment(): Int { // π Thread-safe increment
synchronized(lock) { // π Lock access
return ++count // ✅ Increment and return
}
}
fun getCount(): Int = synchronized(lock) { count } // π Thread-safe read
}
fun main() = runBlocking { // π Simulate multi-threading
repeat(3) { // π Launch 3 coroutines
launch { // π§΅ Coroutine
println("Incremented to ${Counter.INSTANCE.increment()}") // π Increment
}
}
println("Final count: ${Counter.INSTANCE.getCount()}") // Outputs: Incremented to 1, Incremented to 2, Incremented to 3, Final count: 3 π
}
Thread Safety Benefits:
- π§΅ Safe Constants: Enum instances are thread-safe singletons. ✅
- π Synchronized Mutability: Use synchronized for mutable properties to prevent race conditions. π
- ⚡ Use Case: Counters, shared state in multi-threaded apps. π§
- π‘️ Robustness: Ensures consistent state across threads. π«
Thread Safety Tip: Synchronize access to mutable enum properties in concurrent environments to maintain thread safety. π
Practical Use Case: API Response Handling π
Enums can model API response statuses with associated data, paired with when for clean handling. π‘
Example: API Response Enum
enum class ApiResponse(val code: Int) { // π§ Constructor
SUCCESS(200) { // ✅ Success
override fun message() = "Request succeeded"
},
ERROR(500) { // π« Error
override fun message() = "Server error"
},
NOT_FOUND(404) { // π Not found
override fun message() = "Resource not found"
};
abstract fun message(): String // π Response message
companion object { // π§© Utility
fun fromCode(code: Int) = values().find { it.code == code } ?: ERROR // π Find by code
}
}
fun main() {
val response = ApiResponse.fromCode(404) // ✅ Select response
println(when (response) { // π€ Handle response
ApiResponse.SUCCESS -> "${response.message()} (Code: ${response.code})"
ApiResponse.ERROR -> "${response.message()} (Code: ${response.code})"
ApiResponse.NOT_FOUND -> "${response.message()} (Code: ${response.code})"
}) // Outputs: Resource not found (Code: 404) π
}
API Handling Benefits:
- π‘ Type-Safe Responses: Enums ensure valid response types. ✅
- π Clear Logic: when expressions handle responses concisely. π
- ⚡ Use Case: HTTP status codes, API states, or error types. π§
- π‘️ Robust: Companion methods simplify parsing and validation. π«
API Tip: Use enums for fixed API response types; use sealed classes for complex, data-rich responses. π
Edge Cases and Error Handling π«
Handle edge cases like invalid enum values or unexpected states to ensure robust code. π‘️
Example: Handling Invalid Enum Values
enum class Color(val rgb: Int) { // π§ Constructor
RED(0xFF0000), // π Red
GREEN(0x00FF00), // π Green
BLUE(0x0000FF); // π Blue
companion object { // π§© Utility
fun fromRgbOrDefault(rgb: Int, default: Color = BLUE) = // π Safe lookup
values().find { it.rgb == rgb } ?: default
}
}
fun main() {
val color = Color.fromRgbOrDefault(0xFFFFFF) // ✅ Invalid RGB
println(color) // Outputs: BLUE π
println(Color.fromRgbOrDefault(0xFF0000)) // Outputs: RED π
}
Edge Case Benefits:
- π‘️ Safe Lookups: Provide defaults for invalid inputs. ✅
- π Validation: Companion methods handle parsing safely. π
- ⚡ Use Case: Parsing external data (e.g., API responses, user input). π§
- π« Robustness: Prevents null or undefined states. π‘️
Error Handling Tip: Use companion object methods with defaults or nullables to handle invalid enum lookups gracefully. π
Performance Considerations ⚙️
Enums are lightweight but require careful design for performance:
- ⚡ Singleton Efficiency: Enum constants are singletons, minimizing memory usage. ✅
- π§° Member Overhead: Avoid excessive properties or complex methods to keep enums lean. π§Ή
- π Thread Safety: Synchronize mutable properties in multi-threaded contexts. π§΅
- π When Efficiency: when expressions are optimized for enums, especially with exhaustiveness. π
Performance Tip: Profile enum usage in performance-critical sections, keeping methods lightweight and synchronizing mutable state as needed. π
Best Practices for Enum Classes ✅
- π§Ή Keep Simple: Use basic enums for simple constants; add constructors or members only when needed. π«
- ✅ Clear Naming: Use descriptive constant names (e.g., SUCCESS, not S) for readability. π
- π§ Minimal Mutability: Limit mutable properties to avoid unpredictable state changes. π‘️
- π§© Companion Utilities: Add lookup or factory methods in companion objects for flexibility. π
- π€ Use When: Leverage when expressions for clean, exhaustive enum handling. ⚖️
- π§΅ Thread Safety: Synchronize mutable properties in concurrent environments. π
- π Document Logic: Comment complex enum members or companion methods for team collaboration. π§π»
Frequently Asked Questions (FAQ) ❓
- Why use enums instead of constants? π€
Enums are type-safe, support constructors and methods, and pair with when for robust logic, unlike plain constants. π - Can enums have mutable properties? π
Yes, use var properties for mutable state, but synchronize in multi-threaded contexts to avoid race conditions. π§΅ - How do enums differ from sealed classes? ⚖️
Enums are for fixed, singleton constants; sealed classes support complex, data-rich hierarchies with multiple instances. π§© - Why use a companion object in enums? π§©
Companion objects provide shared utilities like lookups or factories, enhancing enum usability. π - Are enums thread-safe? π§΅
Enum instances are thread-safe singletons, but mutable properties require synchronization for thread safety. π - How do I handle invalid enum values? π«
Use companion object methods with defaults or nullables to safely handle invalid inputs. π‘️