How do I use enums and associate data or behavior in Kotlin?

In Kotlin, enum class is used for a fixed set of constants. Enums can have:

  • simple constants
  • constructor parameters / associated data
  • properties
  • functions
  • overridden behavior per constant
  • companion object utilities
  • implemented interfaces

Basic enum

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

Usage:

val direction = Direction.NORTH

when (direction) {
    Direction.NORTH -> println("Going up")
    Direction.SOUTH -> println("Going down")
    Direction.EAST -> println("Going right")
    Direction.WEST -> println("Going left")
}

Kotlin when is exhaustive for enums if you cover all constants, so you often do not need an else.


Enum with associated data

Enums can have a constructor.

enum class HttpStatus(val code: Int, val reason: String) {
    OK(200, "OK"),
    CREATED(201, "Created"),
    BAD_REQUEST(400, "Bad Request"),
    NOT_FOUND(404, "Not Found"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error")
}

Usage:

val status = HttpStatus.NOT_FOUND

println(status.code)   // 404
println(status.reason) // Not Found

Important syntax rule: if an enum has members after the constants, the constant list must end with a semicolon.

enum class HttpStatus(val code: Int) {
    OK(200),
    NOT_FOUND(404);

    fun isSuccess(): Boolean = code in 200..299
}

Enum with shared behavior

You can define functions inside the enum class.

enum class Planet(val mass: Double, val radius: Double) {
    EARTH(5.972e24, 6.371e6),
    MARS(6.39e23, 3.389e6),
    JUPITER(1.898e27, 6.9911e7);

    fun surfaceGravity(): Double {
        val gravitationalConstant = 6.67430e-11
        return gravitationalConstant * mass / (radius * radius)
    }
}

Usage:

println(Planet.EARTH.surfaceGravity())

Enum constants with different behavior

Each enum constant can override functions.

enum class Operation {
    PLUS {
        override fun apply(a: Int, b: Int): Int = a + b
    },
    MINUS {
        override fun apply(a: Int, b: Int): Int = a - b
    },
    TIMES {
        override fun apply(a: Int, b: Int): Int = a * b
    },
    DIVIDE {
        override fun apply(a: Int, b: Int): Int = a / b
    };

    abstract fun apply(a: Int, b: Int): Int
}

Usage:

val result = Operation.TIMES.apply(6, 7)
println(result) // 42

This pattern is useful when the enum represents a strategy or command.


Enum implementing an interface

Enums can implement interfaces.

interface Printable {
    fun label(): String
}

enum class Priority : Printable {
    LOW {
        override fun label(): String = "Low priority"
    },
    MEDIUM {
        override fun label(): String = "Medium priority"
    },
    HIGH {
        override fun label(): String = "High priority"
    }
}

Usage:

val priority: Printable = Priority.HIGH
println(priority.label())

You can also combine constructor data with an interface:

interface HasCode {
    val code: Int
}

enum class ErrorType(
    override val code: Int,
    val message: String
) : HasCode {
    VALIDATION(100, "Validation failed"),
    AUTHENTICATION(200, "Authentication failed"),
    NOT_FOUND(300, "Resource not found")
}

Companion object lookup helpers

A common pattern is looking up enum values by associated data.

enum class HttpStatus(val code: Int) {
    OK(200),
    CREATED(201),
    BAD_REQUEST(400),
    NOT_FOUND(404);

    companion object {
        fun fromCode(code: Int): HttpStatus? {
            return entries.find { it.code == code }
        }
    }
}

Usage:

val status = HttpStatus.fromCode(404)
println(status) // NOT_FOUND

In modern Kotlin, prefer entries over values():

HttpStatus.entries

instead of:

HttpStatus.values()

Built-in enum properties and functions

Every enum constant has:

val name: String
val ordinal: Int

Example:

enum class Color {
    RED,
    GREEN,
    BLUE
}

println(Color.RED.name)    // RED
println(Color.RED.ordinal) // 0

You can parse by name:

val color = enumValueOf<Color>("RED")
println(color) // RED

Or safely:

val color = Color.entries.find { it.name == "RED" }

Enum with custom display names

Avoid relying on name for user-facing text. Use a property instead.

enum class UserRole(val displayName: String) {
    ADMIN("Administrator"),
    EDITOR("Editor"),
    VIEWER("Viewer")
}

Usage:

println(UserRole.ADMIN.displayName) // Administrator

Enum with properties and computed values

enum class FileType(val extension: String) {
    TEXT("txt"),
    JSON("json"),
    CSV("csv");

    val mimeType: String
        get() = when (this) {
            TEXT -> "text/plain"
            JSON -> "application/json"
            CSV -> "text/csv"
        }
}

Usage:

println(FileType.JSON.extension) // json
println(FileType.JSON.mimeType)  // application/json

When to use enums

Use an enum when:

  • the set of values is fixed
  • each value is a singleton
  • you need exhaustive when handling
  • the values are known at compile time

Good examples:

enum class LogLevel {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR
}
enum class PaymentStatus {
    PENDING,
    PAID,
    FAILED,
    REFUNDED
}

When not to use enums

If each variant needs different state shapes, consider a sealed class or sealed interface.

For example, this is better as a sealed type:

sealed interface UiState {
    data object Loading : UiState
    data class Success(val data: String) : UiState
    data class Error(val message: String) : UiState
}

Because Success and Error need per-instance data, not fixed singleton enum constants.


Quick summary

enum class Status(val code: Int) {
    ACTIVE(1),
    DISABLED(2),
    DELETED(3);

    fun isVisible(): Boolean = this != DELETED

    companion object {
        fun fromCode(code: Int): Status? =
            entries.find { it.code == code }
    }
}

Usage:

val status = Status.fromCode(1)

if (status?.isVisible() == true) {
    println("Show item")
}

Kotlin enums are best for fixed named values, and they can carry data and behavior just like small classes.

How do I use object expressions and object declarations in Kotlin?

Object expressions vs. object declarations in Kotlin

Kotlin has two closely related features:

  • Object expressions: create an anonymous object immediately.
  • Object declarations: create a named singleton object.

They both use the object keyword, but they are used for different purposes.


1. Object expressions

Use an object expression when you need a one-off object, often to implement an interface or extend a class without creating a named class.

Basic anonymous object

fun main() {
    val user = object {
        val name = "Ava"
        val age = 30

        fun greet() {
            println("Hello, my name is $name")
        }
    }

    println(user.name)
    user.greet()
}

Here, user is an anonymous object with properties and functions.


2. Implementing an interface with an object expression

Object expressions are commonly used for callbacks, listeners, and small implementations.

interface ClickListener {
    fun onClick()
}

fun setClickListener(listener: ClickListener) {
    listener.onClick()
}

fun main() {
    setClickListener(object : ClickListener {
        override fun onClick() {
            println("Button clicked")
        }
    })
}

The syntax is:

object : SomeInterface {
    override fun someFunction() {
        // implementation
    }
}

3. Extending a class with an object expression

You can also create an anonymous subclass.

open class Animal(val name: String) {
    open fun speak() {
        println("$name makes a sound")
    }
}

fun main() {
    val dog = object : Animal("Buddy") {
        override fun speak() {
            println("$name barks")
        }
    }

    dog.speak()
}

4. Implementing multiple types

An object expression can extend one class and implement one or more interfaces.

open class Logger {
    open fun log(message: String) {
        println("Log: $message")
    }
}

interface Closeable {
    fun close()
}

fun main() {
    val resource = object : Logger(), Closeable {
        override fun log(message: String) {
            println("Custom log: $message")
        }

        override fun close() {
            println("Resource closed")
        }
    }

    resource.log("Started")
    resource.close()
}

If a superclass has a constructor, call it after the class name:

object : Logger()

5. Object declarations

Use an object declaration when you want a named singleton: exactly one instance, created lazily when first used.

object DatabaseConfig {
    val url = "jdbc:postgresql://localhost:5432/app"
    val username = "admin"

    fun connect() {
        println("Connecting to $url as $username")
    }
}

fun main() {
    DatabaseConfig.connect()
}

You do not instantiate it with DatabaseConfig().

Use it directly by name:

DatabaseConfig.connect()

6. Object declarations can implement interfaces

interface Analytics {
    fun track(event: String)
}

object ConsoleAnalytics : Analytics {
    override fun track(event: String) {
        println("Tracking event: $event")
    }
}

fun main() {
    ConsoleAnalytics.track("AppOpened")
}

This is useful for global services, registries, configuration, or strategy objects.


7. Object declarations can extend classes

open class AppLogger {
    open fun info(message: String) {
        println("INFO: $message")
    }
}

object Logger : AppLogger() {
    override fun info(message: String) {
        println("[App] $message")
    }
}

fun main() {
    Logger.info("Application started")
}

8. Companion objects

A companion object is an object declaration inside a class. It is commonly used for factory methods and static-like members.

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            return User(name.trim())
        }
    }
}

fun main() {
    val user = User.create("  Ava  ")
    println(user.name)
}

You call companion object members through the class name:

User.create("Ava")

9. Named companion objects

A companion object can have a name.

class App {
    companion object Config {
        const val VERSION = "1.0.0"

        fun printVersion() {
            println("Version: $VERSION")
        }
    }
}

fun main() {
    App.printVersion()
    App.Config.printVersion()
}

Both calls are valid:

App.printVersion()
App.Config.printVersion()

10. Object expression visibility detail

If an anonymous object is stored in a local variable, you can access its members:

fun main() {
    val person = object {
        val name = "Mira"
        fun sayHi() = println("Hi, I am $name")
    }

    println(person.name)
    person.sayHi()
}

But if an anonymous object is returned from a public function, its specific members are not visible unless the return type exposes them.

class Factory {
    fun createPublic(): Any = object {
        val name = "Hidden"
    }

    private fun createPrivate() = object {
        val name = "Visible inside class"
    }

    fun demo() {
        val item = createPrivate()
        println(item.name)
    }
}

In this example:

fun createPublic(): Any = object {
    val name = "Hidden"
}

The caller only sees Any, not name.


11. Quick comparison

Feature Object expression Object declaration
Purpose One-off anonymous object Named singleton
Has a name? Usually no Yes
Instantiated with constructor? No No
Created when? Immediately where expression runs Lazily on first access
Common use Callbacks, temporary implementations Singletons, global config, shared services
Can extend classes? Yes Yes
Can implement interfaces? Yes Yes

Simple rule of thumb

Use an object expression when you need an object right here, right now:

val listener = object : ClickListener {
    override fun onClick() {
        println("Clicked")
    }
}

Use an object declaration when you need one shared named instance:

object AppSettings {
    val theme = "dark"
}

How do I create and use companion objects for static-like behavior in Kotlin?

In Kotlin, a companion object is an object declared inside a class that can hold members callable on the class itself, giving you static-like behavior.

Kotlin does not have Java-style static members directly. Instead, you usually use companion object.

Basic example

class User(val name: String) {
    companion object {
        const val DEFAULT_NAME = "Guest"

        fun createDefault(): User {
            return User(DEFAULT_NAME)
        }
    }
}

You can access companion object members using the class name:

fun main() {
    println(User.DEFAULT_NAME)

    val user = User.createDefault()
    println(user.name)
}

Output:

Guest
Guest

Why it feels like static

This:

User.createDefault()

is similar to calling a static method in Java:

User.createDefault();

But internally, Kotlin’s companion object is an actual singleton object associated with the class.

Companion object properties

You can put properties inside a companion object:

class Counter {
    companion object {
        var count = 0

        fun increment() {
            count++
        }
    }
}

Usage:

fun main() {
    Counter.increment()
    Counter.increment()

    println(Counter.count)
}

Output:

2

Named companion objects

A companion object can have a name:

class Database {
    companion object Factory {
        fun connect(): Database {
            return Database()
        }
    }
}

You can still access members through the class name:

val db = Database.connect()

Or through the companion object name:

val db = Database.Factory.connect()

Factory method example

A common use case is creating factory methods:

class Person private constructor(
    val name: String,
    val age: Int
) {
    companion object {
        fun of(name: String, age: Int): Person {
            require(age >= 0) { "Age cannot be negative" }
            return Person(name, age)
        }
    }
}

Usage:

fun main() {
    val person = Person.of("Alice", 30)
    println(person.name)
}

Constants in companion objects

For compile-time constants, use const val:

class ApiConfig {
    companion object {
        const val BASE_URL = "https://api.example.com"
        const val TIMEOUT_SECONDS = 30
    }
}

Usage:

println(ApiConfig.BASE_URL)

const val can only be used with primitive types and String.

Java interoperability

From Java, companion object members are normally accessed through Companion:

class MathUtils {
    companion object {
        fun double(x: Int): Int = x * 2
    }
}

Java usage:

int result = MathUtils.Companion.double(5);

If you want Java callers to use it like a real static method, add @JvmStatic:

class MathUtils {
    companion object {
        @JvmStatic
        fun double(x: Int): Int = x * 2
    }
}

Then Java can call:

int result = MathUtils.double(5);

For constants:

class Constants {
    companion object {
        const val APP_NAME = "MyApp"
    }
}

Java can access this as:

String appName = Constants.APP_NAME;

Companion objects can implement interfaces

Because companion objects are real objects, they can implement interfaces:

interface Parser<T> {
    fun parse(value: String): T
}

class User(val name: String) {
    companion object : Parser<User> {
        override fun parse(value: String): User {
            return User(value)
        }
    }
}

Usage:

fun main() {
    val user = User.parse("Alice")
    println(user.name)
}

You can also pass the companion object where the interface is expected:

fun <T> parseWith(parser: Parser<T>, value: String): T {
    return parser.parse(value)
}

val user = parseWith(User, "Bob")

Here, User refers to the companion object when used as a value.

Key points

  • Use companion object for static-like members.
  • Access members as ClassName.member.
  • Use const val for compile-time constants.
  • Use @JvmStatic if Java callers need static-style access.
  • Companion objects are real singleton objects.
  • Companion objects can have names and implement interfaces.

How do I use inner and nested classes in Kotlin?

In Kotlin, classes can be declared inside other classes in two main ways:

  1. Nested classes — default behavior
  2. Inner classes — declared with the inner keyword

Nested classes

A class declared inside another class is nested by default.

class Outer {
    class Nested {
        fun message(): String {
            return "Hello from Nested"
        }
    }
}

You create an instance of the nested class using the outer class name:

fun main() {
    val nested = Outer.Nested()
    println(nested.message())
}

Important point

A nested class does not have access to members of the outer class.

class Outer {
    private val name = "Outer"

    class Nested {
        fun printName() {
            // println(name) // Error: cannot access outer class member
        }
    }
}

This is similar to a static nested class in Java.


Inner classes

If you want the nested class to access members of the outer class, mark it with inner.

class Outer {
    private val name = "Outer"

    inner class Inner {
        fun message(): String {
            return "Hello from $name"
        }
    }
}

You create an inner class instance from an instance of the outer class:

fun main() {
    val outer = Outer()
    val inner = outer.Inner()

    println(inner.message())
}

Output:

Hello from Outer

Difference between nested and inner classes

Feature Nested class Inner class
Keyword No keyword needed Uses inner
Has reference to outer class No Yes
Can access outer members No Yes
Instantiation Outer.Nested() Outer().Inner()
Similar to Java static nested class non-static inner class

Accessing this from an inner class

Inside an inner class, this refers to the inner class instance.

To refer to the outer class instance, use this@Outer.

class Outer {
    private val value = "Outer value"

    inner class Inner {
        private val value = "Inner value"

        fun printValues() {
            println(value)
            println(this.value)
            println([email protected])
        }
    }
}

Output:

Inner value
Inner value
Outer value

Example with state

class ShoppingCart {
    private val items = mutableListOf<String>()

    fun addItem(item: String) {
        items.add(item)
    }

    inner class Summary {
        fun printSummary() {
            println("Cart has ${items.size} items")
            println(items.joinToString())
        }
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.addItem("Book")
    cart.addItem("Pen")

    val summary = cart.Summary()
    summary.printSummary()
}

Output:

Cart has 2 items
Book, Pen

Here, Summary is an inner class because it needs access to items from ShoppingCart.


When to use each

Use a nested class when:

  • The class is logically grouped inside another class
  • It does not need access to the outer class instance
  • You want a namespace-like structure
class ApiResponse {
    class Error(val code: Int, val message: String)
}

Use an inner class when:

  • The class needs access to the outer class’s properties or functions
  • Each inner class instance is tied to a specific outer class instance
class Form {
    private val fields = mutableListOf<String>()

    inner class Validator {
        fun validate(): Boolean {
            return fields.isNotEmpty()
        }
    }
}

Summary

class Outer {
    class Nested {
        // No access to Outer instance
    }

    inner class Inner {
        // Has access to Outer instance
    }
}

Use nested classes by default, and use inner only when the inner class needs to access the outer class instance.

How do I use abstract classes and methods in Kotlin?

In Kotlin, an abstract class is a class that cannot be instantiated directly. It is meant to be subclassed.

An abstract method is a method declared without an implementation. Subclasses must override it.

Basic example

abstract class Animal {
    abstract fun makeSound()

    fun sleep() {
        println("Sleeping...")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        println("Woof!")
    }
}

fun main() {
    val dog = Dog()
    dog.makeSound()
    dog.sleep()
}

Output:

Woof!
Sleeping...

Key points

1. Use abstract before the class

abstract class Shape

You cannot create an instance of it:

val shape = Shape() // Error

2. Abstract methods have no body

abstract fun area(): Double

A subclass must implement them:

class Circle(val radius: Double) : Shape() {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

3. Abstract classes can have regular methods

abstract class Shape {
    abstract fun area(): Double

    fun describe() {
        println("This is a shape")
    }
}

4. Abstract properties are allowed

abstract class Vehicle {
    abstract val maxSpeed: Int
}

class Car : Vehicle() {
    override val maxSpeed: Int = 200
}

5. Abstract classes can have constructors

abstract class Person(val name: String) {
    abstract fun work()
}

class Developer(name: String) : Person(name) {
    override fun work() {
        println("$name writes code")
    }
}

Complete example

abstract class Shape(val name: String) {
    abstract fun area(): Double

    fun printInfo() {
        println("$name has area ${area()}")
    }
}

class Rectangle(
    name: String,
    val width: Double,
    val height: Double
) : Shape(name) {
    override fun area(): Double {
        return width * height
    }
}

class Circle(
    name: String,
    val radius: Double
) : Shape(name) {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

fun main() {
    val shapes = listOf(
        Rectangle("Rectangle", 5.0, 3.0),
        Circle("Circle", 2.0)
    )

    for (shape in shapes) {
        shape.printInfo()
    }
}

Abstract class vs interface

Use an abstract class when you want to share state or constructor logic:

abstract class BaseRepository(val tableName: String) {
    abstract fun findAll(): List<String>
}

Use an interface when you mainly want to define behavior:

interface Drawable {
    fun draw()
}

A class can extend only one abstract class, but it can implement multiple interfaces:

abstract class Animal

interface Runnable {
    fun run()
}

interface Swimmable {
    fun swim()
}

class Duck : Animal(), Runnable, Swimmable {
    override fun run() {
        println("Duck runs")
    }

    override fun swim() {
        println("Duck swims")
    }
}

In short: use abstract class for a shared base with common implementation/state, and use abstract fun or abstract val for members subclasses must provide.