In many programming languages, enums (short for enumerations) are often used simply as lists of predefined constants. However, in Kotlin, enums are much more than that; they are full-fledged classes with advanced capabilities that go beyond just structured data types.
Kotlin’s enum classes are powerful and feature-rich, offering built-in properties and methods that developers can leverage in their projects. Interestingly, they can also be used to create custom properties and methods (similar to how regular classes work), implement interfaces, and serve as anonymous classes.
This guide will explore these capabilities in detail, providing foundational knowledge and practical insights on using Kotlin enums effectively. Whether you’re new to Kotlin or looking to deepen your understanding, this resource will help you integrate Enum classes seamlessly into your coding practices.
What Are Enums in Kotlin?
Enums in Kotlin are special classes used to represent a fixed set of related values. Unlike strings or integers, they offer type safety by ensuring only predefined values are used—which helps reduce the risk of errors. This improves code reliability and maintainability by detecting issues at compilation time that would have caused unexpected failures at runtime.
Additionally, they are more powerful than basic constants because they can have properties, methods, and even implement interfaces, making them highly versatile.
Defining and Initializing Kotlin Enums
How to Define Enum Classes in Kotlin
In Kotlin, enums are defined using the enum keyword followed by the class keyword and the enum name Usually it is customary to provide the name of the enum in PascalCase while the value keys are provided in UPPER_SNAKE_CASE.
Here’s an example:
enum class Mood(val description: String) {
ANGRY("Feeling frustrated or upset"),
SAD("Feeling down or unhappy"),
BORED("Feeling uninterested or restless"),
HAPPY("Feeling joyful and content"),
EXCITED("Feeling thrilled and energetic")
}
fun main() {
val myMood = Mood.HAPPY
println("My mood today is: ${myMood.description}") // My mood today is: Feeling joyful and content
}
Initializing Enum Constants
In Kotlin, enums aren’t just simple constants—they can store and manage structured data. They function more like regular classes in the sense that each enum constant can hold its own properties, making them complex data structures that enforce a fixed set of values.
For example, consider an Animal enum where each constant stores both a sound and the number of legs:
enum class Animal(val sound: String, val legs: Int) {
CAT("Meow", 4),
SPIDER("Hiss", 8),
}
fun main() {
println(Animal.CAT.sound) // Meow
println(Animal.CAT.legs) // 4
}
We achieved this by defining a primary constructor inside the parentheses () between the enum name and the opening curly brace {}. That way, each enum constant stores its own values, treating them as actual instances of the class rather than just simple constants.
The Role of Enum Classes in Kotlin Development
Make Code Easier to Read and Maintain
Using enums in Kotlin makes your code cleaner, more reliable, and less prone to errors. Instead of relying on plain strings, enums provide a predefined set of constants that help enforce type safety.
Here’s a simple example of how enums improve clarity and reliability:
enum class Gadgets { SMARTPHONE, TABLET, LAPTOP, HEADPHONE, SPEAKER }
fun selectGadgetType(type: Gadgets) {
when (type) {
Gadgets.SMARTPHONE -> println("You selected Smartphone")
Gadgets.TABLET -> println("You selected Tablet ")
Gadgets.LAPTOP -> println("You selected Laptop")
Gadgets.HEADPHONE -> println("You selected Headphone")
Gadgets.SPEAKER -> println("You selected Speaker")
}
}
fun main() {
selectGadgetType(Gadgets.SMARTPHONE) //You selected Smartphone
}
When you run this code, the output will be:
You selected Smartphone.
The Problem with Strings
Now, let’s see what happens when we use strings instead:
fun selectGadgetType(type: String) {
when (type) {
"SMARTPHONE" -> println("You selected Smartphone")
"TABLET" -> println("You selected Tablet")
"LAPTOP" -> println("You selected Laptop")
"HEADPHONE" -> println("You selected Headphone")
"SPEAKER" -> println("You selected Speaker")
}
}
fun main() {
selectGadgetType("Smartphone") //No output
}
The above code won’t print anything to the terminal because “Smartphone” is invalid.
Using strings requires additional processing to ensure a correct match. For example, we need to call .uppercase() on every input and handle unknown values with an else statement:
fun selectGadgetType(type: String) {
when (type.uppercase()) {
"SMARTPHONE" -> println("You selected Smartphone")
"TABLET" -> println("You selected Tablet")
"LAPTOP" -> println("You selected Laptop")
"HEADPHONE" -> println("You selected Headphone")
"SPEAKER" -> println("You selected Speaker")
else -> println("Invalid gadget type")
}
}
fun main() {
selectGadgetType("Smartphone") // Case-insensitive
selectGadgetType("watch") // Invalid input
}
Even with these adjustments, using strings still requires manual checks to ensure valid input, which can be error-prone. Enums eliminate this issue by enforcing a strict set of valid values, making code safer and easier to maintain.
Enforces Type Safety
One of the biggest advantages of enums is type safety. Since they can only hold one of their predefined values, you don’t have to worry about unexpected inputs slipping through. The compiler catches mistakes early, making your code more reliable and preventing issues before they happen.
Properties and Methods of Kotlin Enums
Inbuilt Properties and Methods of Enum Classes
Kotlin enum classes come with built-in properties and functions that make them more powerful and convenient to use. Let’s take a look at some of the key ones.
Inbuilt Properties
1. name: This property returns the name of an enum constant as a String. Take a look at the example below:
enum class Animal {
CAT, DOG, GOAT, FISH, DUCK, LIZARD, DONKEY
}
fun main() {
println(Animal.FISH.name) // Output: FISH
}
2. The ordinal property returns the position (or index) of an enum constant in the order it was defined. It is of type Int.
enum class Animal {
CAT, DOG, GOAT, FISH, DUCK, LIZARD, DONKEY
}
fun main() {
println(Animal.DUCK.ordinal) // Output: 4
}
Inbuilt Methods
- The values() method returns an array of all constants in an enum. It comes in useful when you need to iterate through all the values in an enum.
Take a look at the code snippet below:
enum class Animal {
CAT, DOG, GOAT, FISH, DUCK, LIZARD, DONKEY
}
fun main() {
val animals = Animal.values()
for (animal in animals) {
println(animal)
}}
Output:
CAT
DOG
GOAT
FISH
DUCK
LIZARD
DONKEY
This method makes it easy to loop through an enum and access all of its constants.
1. The valueOf() method takes a string and returns the corresponding enum constant if it exists. It’s useful for checking if a given string matches an enum value. However, if the string doesn’t match any constant, it throws an IllegalArgumentException.
enum class Animal {
CAT, DOG, GOAT, FISH, DUCK, LIZARD, DONKEY
}
fun main() {
val myAnimal = Animal.valueOf("GOAT")
//val myAnimal = Animal.valueOf("BIRD")
println(myAnimal) // Output: GOAT
}
In the code above, if we run the commented-out line instead, an exception stating “No enum constant Animal.BIRD” will be thrown.
2. The entries method works similarly to values(), but instead of returning an array, it provides an immutable list of enum constants. Since it doesn’t create a new array each time it’s called, the entries method is a safer and more efficient option. You’ll find this method is available in Kotlin 1.9 and later.
Adding Custom Properties and Defining Member Functions within Enum Classes
Just like regular classes in Kotlin, enum classes can have custom properties and methods. These properties are useful for storing additional information about each enum constant, while methods allow operations specific to those constants.
Custom properties in an enum class can be declared inside the primary constructor or separately within the enum’s body. Custom methods, on the other hand, are defined inside the body of the enum and operate on the enum constants.
In the example below, we define a custom property (duration) and a custom method (shouldStop()). Instead of using a primary constructor, we use a secondary constructor to initialize the property manually:
enum class TrafficLight {
RED(60),
YELLOW(5),
GREEN(30);
var duration: Int
// Secondary constructor to initialize the duration property
constructor(seconds: Int) {
this.duration = seconds
}
fun shouldStop(): Boolean {
return this == RED || this == YELLOW
}
}
fun main() {
println("Red light duration: ${TrafficLight.RED.duration} seconds") // Output: Red light duration: 60 seconds
println("Should stop at yellow? ${TrafficLight.YELLOW.shouldStop()}") // Output: Should stop at yellow? true
println("Should stop at green? ${TrafficLight.GREEN.shouldStop()}") // Output: Should stop at green? false
}
Utilizing Anonymous Classes within Enums
In Kotlin, enum constants can act as anonymous classes when they override abstract methods defined in the enum body. This is useful when each constant needs its own specific behavior.
Take a look at this example:
enum class Students {
GABRIEL {
override fun performAction() = "Sit!"
},
CYNTHIA {
override fun performAction() = "Stand!"
},
TOM {
override fun performAction() = "Sleep!"
};
abstract fun performAction(): String
}
fun main() {
println(Students.TOM.performAction()) // Output: Sleep!
}
Here, performAction() is an abstract method, and each enum constant (GABRIEL, CYNTHIA, TOM) provides its own unique implementation, effectively turning them into anonymous classes and allowing them to behave differently while still being part of the same enum.
Leveraging When Expressions with Enums
Developers tend to perform different actions based on the specific enum value. In kotlin, they don’t need to write multiple if-else statements to handle any of these actions, instead, they can rely on a more readable and efficient control flow structure called when expressions to perform these tasks.
For example, let’s consider using an enum to represent weekdays, and determine which day is a workday or day off:
enum class Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
fun getDayType(day: Day): String {
return when (day) {
Day.SATURDAY, Day.SUNDAY -> "Work-free day"
else -> "Work day"
}
}
fun main() {
println(getDayType(Day.MONDAY)) // Output: Work day
println(getDayType(Day.SUNDAY)) // Output: Work-free day
}
Here, without the repetitive use of if-else conditions, we use a better alternative, when expression, to categorize each day more effectively.
Another advantage of using when expressions with enums is that we can decide not to use an else statement by explicitly handling every possible enum value. This ensures our code throws no exception, even if we choose to add new values in the future.
For instance, in the example below, we ensure each weekday has a specific description:
enum class Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
fun describeDay(day: Day): String {
return when (day) {
Day.MONDAY -> "First day of the week!"
Day.TUESDAY -> "Second day of the week"
Day.WEDNESDAY -> "Midweek"
Day.THURSDAY -> "Almost there!"
Day.FRIDAY -> "Weekend is coming!"
Day.SATURDAY -> "Relax, it's weekend!"
Day.SUNDAY -> "Enjoy your Sunday! the "
}
}
fun main() {
println(describeDay(Day.MONDAY)) // Output: First day of the week!
}
Since every enum constant is explicitly handled, the when expression is exhaustive, meaning an else statement isn’t needed. If a new constant is added to the Day enum later, the compiler will throw an error, ensuring that all cases are accounted for.
This means that our code becomes clearer, safer, and more maintainable when we use when expressions with our enums.
Enums Implementing Interfaces: A Practical Use Case
In some use cases, enum constants need to perform actions in their own unique way. We can make the enum itself to implement an interface and avoid writing a separate class for each case. In a way, this makes each constant behave like an anonymous class with its own unique functionality, making the code look more organized and a lot easier to manage.
A great example of this is a payment system, where each payment method—such as credit card, PayPal, or cryptocurrency—has its own way of processing transactions. Instead of cluttering the code with multiple if-else checks, we can define an interface and let each payment method handle its own logic.
// Interface for processing payments
interface PaymentMethod {
fun processPayment(amount: Double): String
}
// Enum implementing the interface
enum class PaymentType : PaymentMethod {
CREDIT_CARD {
override fun processPayment(amount: Double) = "Processing $$amount via Credit Card."
},
PAYPAL {
override fun processPayment(amount: Double) = "Processing $$amount via PayPal."
},
CRYPTO {
override fun processPayment(amount: Double) = "Processing $$amount via Cryptocurrency."
}
}
fun main() {
println(PaymentType.CREDIT_CARD.processPayment(50.0)) // Output: Processing $50.0 via Credit Card.
println(PaymentType.CRYPTO.processPayment(100.0)) // Output: Processing $100.0 via Cryptocurrency.
}
Here, we distinctively implement the processPayment() function on each payment method. This makes the code easier to extend and maintain because each payment type controls its own behavior instead of everything being handled in one large function.
Implementing Multiple Interfaces
Furthermore, we can also implement more than one interface on an enum. This comes in handy when we need to handle more than one responsibility for an object. For instance, we may need to implement another interface, FraudCheck, on the PaymentType enum, if each payment method needs to perform a fraud check:
// Interface for processing payments
interface PaymentMethod {
fun processPayment(amount: Double): String
}
// Interface for fraud detection
interface FraudCheck {
fun verifyTransaction(): String
}
// Enum implementing two interfaces
enum class PaymentType : PaymentMethod, FraudCheck {
CREDIT_CARD {
override fun processPayment(amount: Double) = "Processing $$amount via Credit Card."
override fun verifyTransaction() = "Checking for unusual credit card activity."
},
PAYPAL {
override fun processPayment(amount: Double) = "Processing $$amount via PayPal."
override fun verifyTransaction() = "Verifying PayPal account security."
},
CRYPTO {
override fun processPayment(amount: Double) = "Processing $$amount via Cryptocurrency."
override fun verifyTransaction() = "Checking blockchain transaction validity."
}
}
fun main() {
println(PaymentType.PAYPAL.verifyTransaction()) // Output: Verifying PayPal account security.
}
Every payment method has its own way of detecting fraud, and using an enum makes it easy to keep these processes separate without making the code messy.
By letting enums implement interfaces, you can keep things clear and well-organized, especially when each option needs to handle something differently. In conclusion, this approach keeps logic encapsulated and less complex in our projects, whether we are dealing with different payment methods, application states, user roles, etc.
Final Thoughts
Kotlin enums do a lot more than just store constants. They give you tools to structure your code better, whether it’s using primary constructors, anonymous classes, or multiple interfaces. Understanding these features can significantly improve how you structure and manage your code.
If you’re new to Kotlin, this should give you a solid starting point. And if you’re already familiar with enums, hopefully, you’ve picked up a few tricks to make them work even harder for you.