Enumerations, in a form of enum class
declarations, got a bad rep on Android.
In fact, the official documentation straight out
recommends to avoid them.
How rude is that?
At the same time, Effective Java
has a full chapter about enum
.
The situation reminds me of the trolley problem.
Kind of.
In this article, I’ll distance myself from Android specifics and show
useful enum
-related snippets.
Declaration
Naming
Use CamelCase
, don’t be ashamed. I doubt that anyone names
sealed class
using the UPPERCASE
notation.
sealed class Level {
data class HIGH(val value: Long) : Level()
data class LOW(val value: Short) : Level()
}
Enumerations are not so different. Don’t scream in code! We don’t have BASIC syntax constraints.
enum class Fruit {
Apple, Orange
}
Commas
Let’s say we have a Screen
enumeration.
enum class Screen(val analyticsName: String) {
SignIn("Sign In")
}
At some point we need to add another case to the Screen
— SignUp
.
enum class Screen(val analyticsName: String) {
SignIn("Sign In"),
SignUp("Sign Up")
}
Looks fine, but the change from a VCS perspective will be like this:
- SignIn("Sign In")
+ SignIn("Sign In"),
+ SignUp("Sign Up")
This approach makes it difficult to see through SignIn
changes.
The last SignIn
change is now tied to the SignUp
one which doesn’t make sense.
We can solve this by putting commas after each enumeration case.
enum class Screen(val analyticsName: String) {
SignIn("Sign In"),
SignUp("Sign Up"),
}
This way the VCS change becomes minimal.
+ SignUp("Sign Up"),
Usage
Constants Namespacing
The barbarian approach to create namespaces is using prefixes.
companion object {
const val SHARED_PREFERENCE_KEY_TOKEN = "token"
const val SHARED_PREFERENCE_KEY_SESSIONS_COUNT = "sessions-count"
}
The next step — named companion object
.
companion object SharedPreferenceKeys {
const val TOKEN = "token"
const val SESSIONS_COUNT = "sessions-count"
}
There is catch though — a class can have a single companion object
, named or not.
enum
doesn’t have such limitations.
enum class SharedPreferenceKey(val value: String) {
Token("token"),
SessionsCount("sessions-count"),
}
Abstraction
API
Let’s imagine that a backend returns codes in error responses.
data class ErrorResponse(@SerializedName("code") val code: String)
This is a fine declaration but the usage becomes repetitive and error-prone to typos.
if (error.code == "not_found") {
throw RuntimeException()
}
// ... 1_000_000 LOC ...
// Oops!
if (error.code == "not__found") {
throw RuntimeException()
}
Enumerations are perfect for this.
enum class ErrorCode(val value: String) {
@SerializedName("not_found") NotFound,
@SerializedName("unauthorized") Unauthorized,
}
data class ErrorResponse(@SerializedName("code") val code: ErrorCode?)
⚠️ Gson will write unknown
enum
values asnull
— ignoring the Kotlin nullability — since the Java reflection doesn’t know about Kotlin. Make such values nullable and handle them as deserialization errors or use Moshi which will do it automatically.
Android Resources
It is possible to define enum
in XML which is helpful with custom View
implementations.
<declare-styleable name="NavigationBar">
<attr name="navigationIcon">
<enum name="back" value="0"/>
<enum name="close" value="1"/>
<enum name="menu" value="2"/>
</attr>
</declare-styleable>
<NavigationBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
application:navigationIcon="close"/>
Instead of Int
-matching in NavigationBar.kt
it is better
to declare an enum
as a direct XML mirror.
enum class NavigationIcon(val attrValue: Int) {
Back(0),
Close(1),
Menu(2),
}
val defaultIcon = NavigationIcon.Back
val icon = attrs.getInt(R.styleable.NavigationBar_navigationIcon, defaultIcon.attrValue).let { attrValue ->
NavigationIcon.values().find { it.attrValue == attrValue } ?: defaultIcon
}
Even More
Map markers:
enum class Marker(@DrawableRes val icon: Int, val elevation: Int) {
Airport(R.drawable.ic_map_airport, elevation = 4),
Underground(R.drawable.ic_map_underground, elevation = 0),
}
data class ViewState(val marker: Marker, val location: LatLng)
List sections:
enum class Section(@StringRes val title: Int, val from: Int, val to: Int) {
Dozen(R.string.Section_Dozen, from = 0, to = 10),
Dozens(R.string.Section_Dozens, from = 11, to = Int.MAX_VALUE),
}
listOf(1, 2, 3, 42).groupBy { number ->
Section.values().find { it.from <= number && number <= it.to }
}
The idea remains the same — use the declarative enum
nature when it fits.
Iterating
Templates
Let’s say we need to render an HTML page. Since we want to keep application colors and HTML colors in sync, we need to translate resources to RGBA hex values.
This is where the enum
values()
method becomes helpful. We can declare
color associations, go over all of them and get processed CSS.
enum class CssTemplateColor(val mask: String, @ColorRes val res: Int) {
Background("color_background", R.color.white),
Text("color_text", R.color.black),
}
val cssTemplate =
"""
body {
background-color: {{color_background}};
color: {{color_text}};
}
"""
val css = CssTemplateColor.values().fold(cssTemplate) { css, color ->
css.replace("{{${color.mask}}}", "#${color.res.hexRgba()}")
}
Tests
This is not as useful for JUnit-based approaches but enumerations shine with anything specification-related.
ErrorCode.values().forEach { errorCode ->
it("creates error response from error code [${errorCode.name}]") {
assertThat(api.response(errorCode)).isEqualTo(ErrorResponse(errorCode))
}
}
This is a rough equivalent of this JUnit test:
@Test fun `it creates error response`() {
ErrorCode.values().forEach { errorCode ->
assertThat(api.response(errorCode)).isEqualTo(ErrorResponse(errorCode))
}
}
However, instead of a single test with a number of assertions the specification above will produce a number of tests with a single assertion in each one.
.
├── it creates error response from error code [BadRequest]
├── it creates error response from error code [NotFound]
└── it creates error response from error code [Unauthorized]
EnumSet
Use it when there is a need to define a subset of enumeration values.
It is much more efficient than regular Set
implementations
since values are stored as bit vectors internally.
val onboardingScreens = EnumSet.of(Screen.SignIn, Screen.SignUp)
This is a java.util
class and unfortunately there nothing
like this in Kotlin for cases when the endgame is multiplatform.
Most likely it is possible to solve this using platform-specific declarations.
expect fun <T> enumSetOf(vararg elements: T): Set<T>
// JVM
actual fun <T> enumSetOf(vararg elements: T): Set<T> = EnumSet.of(elements)
// Not JVM
actual fun <T> enumSetOf(vararg elements: T): Set<T> = setOf(elements)
Anti-Abusing sealed class
I see the following sealed class
usage from time to time and it kind of hurts.
sealed class Color {
object Red : Color()
object Green : Color()
object Blue : Color()
}
There is no reason for this not to be an enum
.
enum class Color {
Red, Green, Blue
}
Putting semantics aside I want to remind everyone that the Kotlin object
creates a singleton. I doubt that it is desirable to have a lot of static objects.
For example, the sealed class Color
above will be translated into something like this (in Java notation):
public abstract class Color {
private Color() {
}
public static final class Red extends Color {
public static final Color.Red INSTANCE = new Color.Red();
}
public static final class Green extends Color {
public static final Color.Green INSTANCE = new Color.Green();
}
public static final class Blue extends Color {
public static final Color.Blue INSTANCE = new Color.Blue();
}
}
Refactoring IRL
I was lucky to see a convenient piece of the Mozilla Fenix code which I’m gonna refactor step-by-step.
sealed class
→ enum class
There is no need to have a sealed class
with object
cases. Replacing it with enum
.
sealed class RiskLevel {
object Low : RiskLevel()
object Medium : RiskLevel()
object High : RiskLevel()
}
⬇️
enum class RiskLevel { Low, Medium, High }
Methods → Fields
We can inline method result values into enum
since both are matching operations and nothing else.
private fun getPageForRiskLevel(riskLevel: RiskLevel): Int {
return when (riskLevel) {
RiskLevel.Low -> R.raw.low_risk_error_pages
RiskLevel.Medium -> R.raw.medium_and_high_risk_error_pages
RiskLevel.High -> R.raw.medium_and_high_risk_error_pages
}
}
private fun getStyleForRiskLevel(riskLevel: RiskLevel): Int {
return when (riskLevel) {
RiskLevel.Low -> R.raw.low_and_medium_risk_error_style
RiskLevel.Medium -> R.raw.low_and_medium_risk_error_style
RiskLevel.High -> R.raw.high_risk_error_style
}
}
⬇️
enum class RiskLevel(@RawRes val html: Int, @RawRes val css: Int) {
Low(R.raw.low_risk_error_pages, R.raw.low_and_medium_risk_error_style),
Medium(R.raw.medium_and_high_risk_error_pages, R.raw.low_and_medium_risk_error_style),
High(R.raw.medium_and_high_risk_error_pages, R.raw.high_risk_error_style),
}
Usage
Since we don’t have methods anymore we can reference enum
fields directly.
val htmlResource = getPageForRiskLevel(riskLevel)
val cssResource = getStyleForRiskLevel(riskLevel)
ErrorPages.createErrorPage(htmlResource, cssResource)
⬇️
ErrorPages.createErrorPage(riskLevel.html, riskLevel.css)
Voilà!
Seems like we managed to eliminate about 20 LOC without loss. In fact, the code became more declarative!
I think this example shows that there are good enumeration use cases.
Don’t be afraid to use them. Not everything needs a sealed class
.