🗃️ Shadow-Enum#
A modern, Kotlin Symbol Processor (KSP) based alternative to older reflection-heavy libraries (like Livefront's sealed-enum). It brings true enum-like capabilities (entries, valueOf, name, and ordinal) to sealed classes and interfaces across all Kotlin Multiplatform targets.
🎯 Supported Targets#
The following targets are supported:
| Platform | Targets |
|---|---|
| JVM & Android | jvm, android |
| Apple | ios, macos, tvos, watchos |
| Web | js, wasmJs |
| Native & Other | androidNative, linux, mingw, wasmWasi |
The Problem with Enums and Sealed Classes#
In modern Kotlin development, developers are often forced to choose between two imperfect structures:
enum class: Provides fantastic built-in utilities (.entries,.name, and string parsing viavalueOf()), but cannot hold dynamic state. Every enum constant is a rigid singleton.sealed interface/sealed class: Allows for rich, mixed hierarchies wheredata objectsingletons can exist alongsidedata classinstances holding state. However, sealed classes have zero built-in iterative capabilities. You cannot easily get a list of all objects, parse them from a string, or get their ordinal position without resorting to heavy Reflection.
Because Kotlin Reflection (kotlin-reflect) causes massive binary bloat on Android and completely breaks on Kotlin/Native and Kotlin/Wasm, multiplatform developers are left without a solution.
ShadowEnum solves this by using KSP at compile time to generate the missing bridging code with absolutely zero runtime reflection.
🚀 Installation#
Integration using Version Catalog is highly recommended for aligned version usage.
[libraries]
kommons-shadow-enum = { group = "dev.datlag.kommons", name = "shadow-enum", version.ref = "shadow-enum" }
kommons-shadow-enum-processor = { group = "dev.datlag.kommons", name = "shadow-enum-processor", version.ref = "shadow-enum" }
Then add the dependency to your module:
plugins {
alias(libs.plugins.ksp)
}
dependencies {
implementation(libs.kommons.shadow.enum)
add("kspCommonMainKotlinMetadata", libs.kommons.shadow.enum.processor)
}
🛠️ Usage and the "Shadow Enum" Architecture#
To use the library, simply apply the @EnumShadow annotation to any sealed interface or class. You should also define an companion object, which serves as the anchor point for the generated extension functions.
@EnumShadow
sealed interface AppTheme {
data object Light : AppTheme
data object Dark : AppTheme
companion object
}
The Generated Output#
At compile time, the KSP plugin generates a standalone registry object (e.g., AppThemeShadowEnum) that implements the ShadowEnum<T> interface. It also attaches convenient extensions to your companion object.
This allows you to interact with your sealed class exactly as if it were a standard Kotlin enum:
fun main() {
// 1. Iterate over all object entries
AppTheme.entries.forEach { theme ->
println("Theme ${theme.name()} is at index ${theme.ordinal()}")
}
// 2. Safe String Parsing (A feature missing from standard enums!)
val fromApi: AppTheme? = AppTheme.valueOfOrNull("Light")
// 3. Strict String Parsing
val strictApi: AppTheme = AppTheme.valueOf("Dark")
}
🎛️ Advanced: Handling Mixed Hierarchies#
The primary reason developers use sealed classes over standard enums is to mix static singletons with dynamic state.
The ShadowEnum processor natively understands this and categorizes your hierarchy intelligently:
@EnumShadow
sealed interface NetworkResult {
data object Loading : NetworkResult
data class Success(val json: String) : NetworkResult // <-- Has State!
companion object
}
If you query the generated ShadowEnum object, it handles this mixed state safely:
entries: This list will only contain theLoadingobject. Data classes are intentionally excluded fromentriesbecause they are not singletons.classes: This list contains theKClassreferences for all children (bothLoadingandSuccess).ordinalOf(): Calling this onLoadingreturns0. Calling this on aSuccessinstance safely returns-1.nameOf(): Calling this onLoadingreturns"Loading". Calling it on aSuccessinstance elegantly falls back to its simple class name"Success".
🗂️ Advanced: Nested Naming Convention#
When structuring large sealed hierarchies, developers often nest objects inside other sealed interfaces.
By default, the @EnumShadow annotation uses a dot (.) as a nestedSeparator. When the KSP plugin generates the name for a nested object, it automatically concatenates the parent names.
@EnumShadow(nestedSeparator = '/')
sealed interface Route {
sealed interface Auth : Route {
data object Login : Auth
}
companion object
}
In the example above, calling Route.valueOf("Auth/Login") will correctly parse the nested string and return the Login object. You can even use the vararg convenience method: Route.valueOf("Auth", "Login").