Values, not Enums

How I stopped using enums and embraced type-safe value classes.

Renee Vandervelde
4 min readSep 18, 2022

Several years back the Android community went through a lot of discussion about Android’s usage of constants in the API instead of using enum values. At the time, I was team enum. But new tools and more experience has led me to revisit this decision.

Update: I’ve written a follow-up to this article over on my website.

How it started

Android’s original API is heavily dependent on option flags for functionality. Let’s look at some Intent flags:

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

The above code sets the new task flag on an intent to indicate that the activity in question should be launched as a separate windowed task. However, if we look at how that constant is defined, we can see it’s just an integer (268435456, to be precise)
Many people, myself included, did not like the lack of type safety in these arguments. Any integer could be passed into this method and have ambiguous meaning. Including constants that aren’t even intended as argument flags. Why not pass in Intent.EXTRA_DOCK_STATE_CAR?
Android arguably does this for efficiency, and fair enough. For all we know, this is running on a very under-powered watch. And Google even built out compiler tools to help add back-in type safety with annotations like @IntDef
But many people found this solution to be lacking, and argued that the API’s should use enum classes instead. As Kotlin became more popular in Android development, sealed classes became very popular, and a combination of both began to appear among libraries, Google’s included, within the Android community.

How it’s going

Enums and sealed classes brought us more reliable type-safety in our API’s. In addition to compile safety and reliable autocompletion in the IDE, both enums and sealed classes offer argument exhaustion safety, something Kotlin embraced with it’s when syntax:

This can be incredibly useful, and is worth considering for your API. You might get so excited with the possibilities that you forget about the major drawback this introduces: breaking changes.

Consider an enum for the above LightType class:

Making this type an enum presents a real problem: We cannot add a product without a breaking change in our API. Generally, I want to be able to add features without a breaking change in our library. Making this type an enum or sealed class will prevent that.

You could just accept this and increase the version number every time one of these constructs needs a new value. But what we really want is a way to keep the type-safety of an enum, but without allowing exhaustive switching like kotlin’s when statement. Some way to denote that this collection is unstable.

Using Values Like Enums

Kotlin’s syntax doesn’t have an ‘unstable’ denotation for sealed classes or enums. But this doesn’t mean we can’t use the language to create our own. Let’s turn our eyes to another language feature in Kotlin: Value Classes.

Formerly called ‘inline classes’, value classes wrap a value or a primitive and decorate it with methods and give type-safety to arguments. In other words, it wraps a single value. When doing this the compiler can make optimizations that retain the performance characteristics of a primitive value, while giving all of the benefits of a real class. And, because it’s not required to be a member of a sealed class, we can remove the exhaustive requirements that would cause breaking changes in our API.
Let’s look at the LightType example again:

The type can be any value, public or private. The value class defers its identity to this argument. Meaning we can treat this value’s equality just like the string’s:

Our companion object predefines a set of values. However, now that this value class could be comprised of anything, we can no longer use it exhaustively:

This works because the value is effectively open to any value, but we have restricted the values to only the ones defined in our class with a private constructor. Meaning no 3rd party values could actually be introduced.

We can even bring our class a little closer to an enum by maintaining a values collection:

Was it Worth it?

It’s easy to get distracted by the possibilities of newer language features. Building the API for a library is about communicating your intent, and limiting the scope of features to what you plan to support. If you know that a type will only have a constrained set of values, then enums and sealed classes work great — A Result type is a good example of something with a fixed set of values. However, if you want the user of your code to plan for future expansion of types, then these constructs may cause trouble down the road.
Thinking carefully about the constructs used allows you to keep making changes without breaking users’ workflows. Sometimes all you needed was a value.

--

--

Renee Vandervelde

Software Engineer at Stripe. Currently focused on Kotlin and Android.