Noam Silverstein
Noam Silverstein

Reputation: 829

How to extend enums in Kotlin?

In my Kotlin project, I have a DefaultError enum

enum class DefaultError {
    INTERNET_ERROR,
    BLUETOOTH_ERROR,
    TEMPERATURE_ERROR
}

I would like to extend them so that I have

enum class NfcAndDefaultError : DefaultError {
    //DefaultError inherited plus
    NFC_ERROR
}

and another enum

enum class KameraAndDefaultError : DefaultError {
    //DefaultError inherited plus
    CAM_ERROR
}

Now, I have

enum class NfcDefaultError {
    INTERNET_ERROR,
    BLUETOOTH_ERROR,
    TEMPERATURE_ERROR,
    NFC_ERROR
}

and

enum class KameraAndDefaultError {
    INTERNET_ERROR,
    BLUETOOTH_ERROR,
    TEMPERATURE_ERROR,,
    CAM_ERROR
}

I bet Kotlin has a nice way there?

Upvotes: 30

Views: 57155

Answers (6)

Tatsuya Fujisaki
Tatsuya Fujisaki

Reputation: 1971

Simple solution with no inheritance, sealed class, or interface

enum class Fruit(val emoji: String) {
    APPLE("🍎"),
    ORANGE("🍊");
}

enum class ExtendedFruit(emoji: String) {
    APPLE(Fruit.APPLE.emoji),
    ORANGE(Fruit.ORANGE.emoji),
    BANANA("🍌");

    companion object {
        fun create(fruit: Fruit) = entries.first { it.name == fruit.name }
    }
}

Upvotes: 0

Avinash Jeeva
Avinash Jeeva

Reputation: 577

No easy way as of now.

Go with a separate enum and type it with a marker interface

interface Error        // marker interface

enum class DefaultError: Error {
    INTERNET_ERROR,
    BLUETOOTH_ERROR,
    TEMPERATURE_ERROR
}

enum class KameraAndDefaultError: Error {
    INTERNET_ERROR,
    BLUETOOTH_ERROR,
    TEMPERATURE_ERROR,
    CAM_ERROR
}

enum class NfcDefaultError: Error {
    INTERNET_ERROR,
    BLUETOOTH_ERROR,
    TEMPERATURE_ERROR,
    NFC_ERROR
}

Usage:

val error: Error = KameraAndDefaultError.INTERNET_ERROR

Upvotes: 1

Balazs
Balazs

Reputation: 83

You can use sealed interfaces since Kotlin 1.5 to achieve what you want :)

You can find some examples here: https://quickbirdstudios.com/blog/sealed-interfaces-kotlin/

Just ping me if you need some more explanation!

Upvotes: 2

TheOperator
TheOperator

Reputation: 6436

There is more to the reason why enum inheritance is not supported than "inheritance is evil". In fact, a very practical reason:

enum class BaseColor { BLUE, GREEN, RED }

val x: BaseColor = ... // must be one of the 3 enums, right?
// e.g. when {} can be done exhaustively with BLUE, GREEN, RED

enum class DerivedColor : BaseColor { YELLOW }

val y: BaseColor = ... // now, it can also be YELLOW
// here, you lose the guarantee that it's a value in a limited set
// and thus the main advantage of enums

There are multiple options to achieve what you like:

1. Different enums implement a common interface

I would refer you to this answer.

Interfaces are a very flexible solution and allow you to derive an unlimited number of enums. If this is what you need, go for it.

2. Sealed classes

In Kotlin, sealed classes are a generalization of enums, that allows you to retain state in each value. All derived classes of a sealed class must be known up-front and declared in the same file. The advantage compared to interfaces is that you can limit the sealed class to a fixed set of possible types. This allows you to omit the else branch in when, for example. The drawback is that it's not possible to add types outside the sealed class by design.

Semantically, you have an enum of enums: the first level determines which enum class type is used, and the second level determines which enumerator (constant) inside that enum class is used.

enum class DefaultError { INTERNET_ERROR, BLUETOOTH_ERROR, TEMPERATURE_ERROR }
enum class NfcError { NFC_ERROR }
enum class CameraError { CAM_ERROR }

sealed class Error {
    data class Default(val error: DefaultError) : Error()
    data class Nfc(val error: NfcError) : Error()
    data class Camera(val error: CameraError) : Error()
}

fun test() {
    // Note: you can use Error as the abstract base type
    val e: Error = Error.Default(DefaultError.BLUETOOTH_ERROR)

    val str: String = when (e) {
        is Error.Default -> e.error.toString()
        is Error.Nfc -> e.error.toString()
        is Error.Camera -> e.error.toString()
        // no else!
    }
}

Upvotes: 38

Khathuluu
Khathuluu

Reputation: 122

The simple answer is that you can't extend enums in Kotlin the way you would want to.

I have to agree with Miha_x64's comment stating that inheritance is "evil" and should only be used when it legitimately makes sense (yes, there are still situations when inheritance is the way to go). I believe that instead of actually trying to work around the design of enums in Kotlin, why don't we design our solution differently? I mean: Why do you think you need such an enum hierarchy, to begin with? What's the benefit? Why not simply have some "common errors" instead and specific errors for whichever concrete area needs very specific errors? Why even use enums?

If you are dead set on using enums, then Januson's solution might be your best bet, but do please use meaningful error codes because using "1001", "1002", "233245" is so 1980s and utterly horrible to read and work with. I find "INTERNET_ERROR" and "BLUETOOTH_ERROR", etc. just as cryptic... can we really not do any better and be more specific about what went wrong so that whoever reads the error code CAN actually understand what is wrong without needing to dig through the internet or through some hefty documentation for the next many minutes/hours? (except, of course, if there are some legitimate reasons why the code needs to be as small as possible - e.g. message size limitations, bandwidth limitations, etc.)

In case you are not dead set on using enums, then you could consider the following:

data class ErrorCode(
    val code: String,
    val localeKey: String,
    val defaultMessageTemplate: String
)
val TENANT_ACCESS_FORBIDDEN = ErrorCode(
    "TENANT_ACCESS_FORBIDDEN",
    "CommonErrorCodes.TENANT_ACCESS_FORBIDDEN",
    "Not enough permissions to access tenant ''{0}''."
)
val NO_INTERNET_CONNETION = ErrorCode(
    "NO_INTERNET_CONNETION",
    "DeviceErrorCodes.NO_INTERNET_CONNETION",
    "No internet connection."
)
val NO_BLUETOOTH_CONNECTION = ErrorCode(
    "NO_BLUETOOTH_CONNECTION",
    "DeviceErrorCodes.NO_BLUETOOTH_CONNECTION",
    "No bluetooth connection."
)
val TEMPERATURE_THRESHOLD_EXCEEDED = ErrorCode(
    "TEMPERATURE_THRESHOLD_EXCEEDED",
    "DeviceErrorCodes.TEMPERATURE_THRESHOLD_EXCEEDED",
    "Temperature ''{0}'' exceeds the maximum threshold value of ''{1}''."
)

Because all the above codes, in essence, act as static constants, comparing against them is as easy as comparing enums (e.g.: if (yourException.errorCode == NO_INTERNET_CONNECTION) { // do something }).

Inheritance is really not needed, what you really need is a clear separation between common and non-common error codes.

Upvotes: 0

Januson
Januson

Reputation: 4841

You can extend an Enum. Kind of. But not with inheritance. Enums can implement an interface. That means to extend it you would simply add another enum implementing the same interface.

Lets say you have an error. This error has an error code. Default error are implemented as DefaultError enum and can be extended by adding aditional enums implementing the Error interface.

interface Error {
    fun code(): Int
}

enum class DefaultError(private val code: Int) : Error {
    INTERNET_ERROR(1001),
    BLUETOOTH_ERROR(1002),
    TEMPERATURE_ERROR(1003);

    override fun code(): Int {
        return this.code
    }
}

enum class NfcError(private val code: Int) : Error {
    NFC_ERROR(2001);

    override fun code(): Int {
        return this.code
    }
}

enum class KameraError(private val code: Int) : Error {
    CAM_ERROR(3001);

    override fun code(): Int {
        return this.code
    }
}

Upvotes: 32

Related Questions