Enumerated Classes with Associated Data in Kotlin

Video thumbnail

As in other programming languages, Enums in Kotlin are a set of constant values that we can leverage for various operations. For example, they are ideal for defining user states (Authenticated, Regular, Administrator) or any list of elements that do not change.

Unlike other programming languages, enums have a strong link with classes in Kotlin.

We will see practical examples, such as the planets. If your application handles planets, it doesn't always make sense to register them in a database if you only need a constant reference. An Enum works as a list of fixed values that will never change during the execution of the application.

Syntax and Basic Usage

Enumerations are a special data type that allows setting predefined values or constants in variables that are used in other programming languages like Java. We can also use them in Kotlin in a very similar way to other languages like Java, although with a different syntax at the time of definition; let's remember that in Java we must declare our enumerated type or enum as follows:

enum class Planetas { MERCURIO, VENUS, TIERRA, MARTE, JÚPITER, SATURNO, URANO, NEPTUNO; }

To query it:

print(Planetas.MERCURIO) // MERCURIO

It is important to note that comparisons must respect the type. You cannot compare an enumerated type with a String directly; Kotlin will force you to compare Planet with Planet:

print(Planetas.MERCURIO == Planetas.VENUS) // false
print(Planetas.MERCURIO == Planetas.MERCURIO) // false
print(Planetas.MERCURIO == "VENUS") // Operator '==' cannot be applied to 'Planetas' and 'String'.

Enums with Properties and Constructors

Although normally enumerated types or enum can have some type of data or information contained within them—similar to the Pairs in Kotlin that we covered in a previous entry—in Java or the Java way, it would be as follows:

enum class Planetas(val orden: Int)  {
    MERCURIO(1),
    VENUS(2),
    TIERRA(3),
    MARTE(4),
    JÚPITER(5),
    SATURNO(6),
    URANO(7),
    NEPTUNO(8),
    PLANETAX(9);
}

In Kotlin, as we saw in the case of Classes in Kotlin, we can reduce everything to its minimum expression, and indicating the type and name is enough to create an enumerated type or enum:

enum class NombreEnumerado

But the previous code in that form is of little use; to fix this, we have to open the braces and indicate all the possible values for our enumeration; for example:

enum class Planetas {
	MERCURIO, VENUS, TIERRA, MARTE, JUPITER, SATURNO, URANO, NEPTUNO
}

As you can see, it is an object where each of its constants is defined, separated by commas.

In these cases, when we access some of the values defined in the previous enum—for example Planetas.URANO—this would return the name of the planet as output, in this case, a text in uppercase representing the name of the planet:

"URANO"

We can even have different types in enumerated classes, although it might be a bit strange, I'm sure more than one use can be found for it:

MERCURIO(1), VENUS(2), TIERRA(3), MARTE(4), JÚPITER(5), SATURNO(6), URANO(7), NEPTUNO(8), PLANETAX(10);

Another possible use is an event manager in which you are working with different data; for example, for your model you would need to have:

  • Title: text type
  • Content: text type
  • Image: could be a boolean type
  • Date: a datetime could work well

So, if for some reason we wanted to use a Kotlin enum, we would have to handle different types of data, although for this example, data classes in Kotlin could also work well, but it all depends on how you have conceptualized your application. Another example would be a Chat messaging system similar to the previous example; we can still empower this more by defining our own values along with the enums as we will see in the next section.

Defining constant values in enums through a constructor

Each of the definitions of the enumerated types are constant values, so to initialize them, a value must be indicated after the name in parentheses and a constructor method defined as we did previously:

enum class Planetas(val orden: Int) {
	MERCURIO(1), VENUS(2), TIERRA(3), MARTE(4), JUPITER(5), SATURNO(6), URANO(7), NEPTUNO(8)
}

As you can see, the syntax remains very clean with some additional elements, and we continue using the enum class as was done previously. Since it is a constructor method, we can specify as many parameters as we want:

enum class Planetas(val n: Int, val tamano: Float) {
   MERCURIO(1,1F), VENUS(2,2F), TIERRA(3,2F), MARTE(4,1.5F), JUPITER(5,2600F), SATURNO(6,1500F), URANO(7,20F), NEPTUNO(8,20F)
}

Optional values in the constructor:

public enum class Planetas(val nivel: Int, val composition: String = "solid") {
    MERCURIO(1),
    VENUS(2),
    TIERRA(3),
    MARTE(4),
    JÚPITER(5, "gas"),
    SATURNO(6, "gas"),
    URANO(7, "gas"),
    NEPTUNO(8, "gas"),
    PLANETAX(9);
}

fun main() {
	print(Planetas.TIERRA.composition)
}

Although this step (initializing values) is optional, and in many other cases, depending on the model we want to use, we don't have to place it; for example, if we only need a simple reference as we specified at the beginning or in the following example:

enum class Persona { NOMBRE, APELLIDO, EDAD, TAMANO, SEXO }

Or the same example that we can find on the Kotlin website:

enum class Direction { NORTH, SOUTH, WEST, EAST }

As you can see, enumerated data types or enum are very useful when we want to classify different data models for which we do not need to perform operations via methods or other elements, just a mere reference and at most an accompanying value.

Usage with Control Structures (when)

Now, to be able to use some of the enums we saw earlier; for example, the one for the planets in the solar system:

enum class Planetas(val orden: Int) {
   MERCURIO(1), VENUS(2), TIERRA(3), MARTE(4), JUPITER(5), SATURNO(6), URANO(7), NEPTUNO(8)
}
val planeta = Planetas.MARTE

val colorPlaneta = when(planeta) {
   Planetas.TIERRA -> "tierra"
   Planetas.MARTE -> "rojo"
   else -> "Otros colores"
}

println(colorPlaneta)

Overriding methods in enums

We can create our own methods and even override existing ones, for example, the toString() method as follows:

enum class Planetas {
	MERCURIO, VENUS, TIERRA, MARTE, JUPITER, SATURNO, URANO, NEPTUNO;

    override fun toString(): String {
        return super.toString().toUpperCase()
    }
}

Defining abstract methods in enums

And that would be all with enumerated types in Kotlin. You can find more information at the following link: Kotlin: Enum Classes

We can define static methods but in the Kotlin way, with companion objects inside the enum itself to perform, for example, a printout of all the values of the enumerations:

enum class UserEnum {
    USER_ID,
    USERNAME,
    NAME,
    SURNAME,
    EMAIL,
    AUTH_LEVEL,
    AVATAR,
    COUNT_NOTIFICATION;

    companion object {
        inline fun <reified T : Enum<T>> colString() {
            print(enumValues<T>().joinToString { it.name.toLowerCase() })
        }

    }
}

fun main(args: Array<String>) {
    UserEnum.colString<UserEnum>() // print user_id, username, name, surname, email, auth_level, avatar, count_notification
}

Or we can extend functions without the need to implement them inside the enumerated type:

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}
fun main(args: Array<String>) {
    printAllValues<RGB>() // prints RED, GREEN, BLUE
}

Anonymous classes: Supertypes, overriding, and abstract methods in Kotlin:

We can define constant values in enums using functions and anonymous classes as shown below; they are ideal for displaying or performing some calculation related to the rest of the structure we create in the enumerable types, and this is further proof of how versatile and consistent data structures are in Kotlin:

enum class Interes {
    SILVER {
       override fun intereses() = 0.25f
    },
    GOLD {
        override fun intereses() = 0.5f
    };
 
    abstract fun intereses(): Float
}

fun main(args: Array<String>) {
    val cashbackPercent = Interes.SILVER.intereses()
    print(cashbackPercent) // print 0.25
}

Enumerated classes to implement interfaces

Interfaces are a type of mechanism we have in classes, as we discussed in a previous post about class types; interfaces are also available in enumerations and we can use them as follows:

interface Interes  {
       override fun intereses(): Float
}

enum class Interes {
    SILVER {
       override fun intereses() = 0.25f
    },
    GOLD {
        override fun intereses() = 0.5f
    };
 
    abstract fun intereses(): Float
}

fun main(args: Array<String>) {
    val cashbackPercent = Interes.SILVER.intereses()
    print(cashbackPercent) // print 0.25
}

As you can see, we declare an interface called Interes with a method which we then override in the enum that has the same name as the interface.

Iteration of enumerated data types

Similarly, it is very easy to iterate over our constants defined in the enum, by indicating the enum as if it were an array, then using the values function (or entries in newer Kotlin versions) which returns the collection of enums, and we take the value of the enum with the name property:

for (userEnum  in UserEnum.values()) { 
  println(userEnum.name) 
}

As you can see, in this way we can traverse the enums like an array. This is useful since we often need to process the enum as a whole, compare its values directly, and that's what the values method is for; if you want to know the size:

 UserEnum.values().size

If you want to search directly within the collection of enumerations, passing a string as a parameter that matches (or not) with the constant:

 UserEnum.valueOf("NAME")

To know the position of a constant within the collection of enumerations:

println(UserEnum.USERNAME.ordinal) // print 1

Enums within other enums

We can also embed enumerations within one another, and we can apply everything seen so far at any level; for example, an enum inside another enum:

enum class Meses {
    Enero,
    Febrero;

    enum class Dias{
        Lunes,
        Martes,
        Miercoles
    }
}

println(Meses.Enero)
println(Meses.Dias.Lunes)

Conclusion

In this post, we saw how powerful and versatile enumerations are in Kotlin, providing a good guide that will allow you to perform the most common operations with enumerations in Kotlin; knowing basic concepts and initializing constants through constructors, using methods and interfaces, embedding enumerations, traversing them as if they were a list or an array, using companion objects, and many other operations.

Next step, learn how to use Companion Objects to handle Static and Factories in Kotlin

Enumerations are a special data type that allows you to set predefined values ​​or constants in variables that you can use in Kotlin, implement constructors, properties, methods... this entry explains how to use this structure.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español