Skip to main content

The power of sealed classes in Kotlin

Updated:

Wouldn't it be great if you could represent a single type that can come in different forms, each able to be constant or carry their own data? If programming in Kotlin, we are in luck as that is exactly what a sealed class is perfect for!

Enums or enumerated types have existed in many different programming languages for years and allows us to represent a type whose value is taken from a limited set of values. Kotlin takes this concept and evolves into something much more powerful, known as sealed classes. They are a really useful addition to the language, enabling powerful use cases and can help us build some really nice APIs. Let's have a look at how sealed classes can be used, their benefits and some example situations in which they can be the perfect tool for the job.

How

Both enums and sealed classes allow us to represent a type that can be one value from a set of possibilities. An enum consists of a set of constant values and the instance is assigned one of these constants. Sealed classes instead rely on a set of sub-classes, allowing us to have multiple instances of each sub-class and for them to have their own state.

Sealed class sub-classes can carry their own state

Let's say we have an AuthenticationState that keeps track of which state a user's account is in within our app. The user can be: signed in with a user identifier, signed out with some credentials stored or fully signed out.

sealed class AuthState {
    data class SignedIn(val userGuid: UUID) : AuthState()
    data class StoredCredentials(val credentials: Credentials) : AuthState()
    object SignedOut : AuthState()
}

By using a sealed class, a property of type AuthenticationState has to have one of the sub-classes assigned to it. We can use a data class to give the sub-class its own properties or an object and make it constant.

Unlike with an enum, the sub-classes do not need to be kept in the body of the sealed class, only within the same file.

// AuthState.kt

sealed class AuthState
data class SignedIn(val userGuid: UUID) : AuthState()
data class StoredCredentials(val credentials: Credentials) : AuthState()
object SignedOut : AuthState()

The way we choose to structure our sealed classes affects how they are referenced, the above examples resulting in AuthState.SignedOut or SignedOut. We can therefore place the sub-classes within the sealed class to create a namespace for our type.

When

One of the situations sealed classes really stand out is within a when expression. Using when as an expression, assigning or returning the result, allows the compiler to determine if all possible cases have been handled, without needing an else branch.

fun onAuthStateChanged(newState: AuthState) = when (newState) {
    is AuthState.SignedIn -> showSignedIn(newState.userGuid)
    is AuthState.StoredCredentials -> showSignedIn(newState.credentials)
    AuthState.SignedOut -> showSignedOut()
}

The compiler can determine if all cases have been handled

By using sealed classes within our APIs we can make it really easy for consumers to handle all of the possible states. When used within our own code, if we add an extra sub-class then any when statements used as an expression have to handle it.

View state

In Android apps (or other GUI applications), we need a way to connect the logic that controls our UI to the views themselves. One part of this process may involve modelling the state of the view, such as if it is loading, showing an error or showing data.

sealed class ViewState
object LoadingState : ViewState()
data class PresentingState(val viewData: ContactsViewData) : ViewState()
data class ErrorState(val message: String) : ViewState()

Imagine we are developing a screen that displays a list of contacts loaded from a local database. Our view layer will receive the ViewState and can then render the correct views based on which sub-class of the sealed class is received.

fun renderViewState(viewState: ViewState) = when (viewState) {
  LoadingState -> showLoadingViews()
  is PresentingState -> showPresentingViews(viewState.viewData)
  is ErrorState -> showErrorViews(viewState.message)
}

Using a sealed class allows us to update our views for all the possible view states, along with each state carrying a different set of data:

  • LoadingState can just be an object with no data
  • PresentingState brings with it ContactsViewData to render the contacts
  • ErrorState contains an error message to be shown

Without a sealed class, we would need a class that contains all of the data as nullable properties and an enum to represent which state it was in.

Analytics events

We will often need to work out how people are using our apps or verify the effectiveness of certain actions through the use of analytics. This usually boils down to firing off events after specific actions have been taken within the app. These analytics events will be reported in the same way, but may have different properties attached to them, this is a perfect candidate for a sealed class. Code that needs to handle the event can do this easily using a when expression, for example, mapping each event to its map of properties.

sealed class AnalyticsEvent {
    object AccountCreated : AnalyticsEvent()
    data class MessageSent(val conversation: Conversation) : AnalyticsEvent()
    data class ProfileOpened(val participant: Participant) : AnalyticsEvent()

    fun parameters(): Map<Parameter, String> = when (this) {
        AccountCreated -> mapOf()
        is MessageSent -> mapOf(
            Parameter.PARTICIPANT_COUNT to
                conversation.participants.size.toString(),
            Parameter.IS_ARCHIVED to
                conversation.isArchived.toString()
        )
        is ProfileOpened -> mapOf(
            Parameter.HAS_ACCOUNT to
                participant.hasCreatedAccount.toString()
        )
    }

    enum class Parameter {
        HAS_ACCOUNT,
        IS_ARCHIVED,
        PARTICIPANT_COUNT,
    }
}

We could instead model an analytics event as a simple data class with a name and map of properties. An issue with this approach is it can spread the event names, property names and creation of events all over the codebase. The advantage of the sealed class approach is that we can keep the definition of all our events in one place, ensuring they are correct and that they report the correct properties. It is really great that sealed classes allow us the flexibility to have an AnalyticsEvent type that can be passed around, even though it can come in many different forms.

Wrap up

Sealed classes are a really powerful feature of Kotlin, allowing the flexibility to model a single type that can come in a finite set of different forms. We have only explored a few of the use cases for them, I am sure you will find many more in your codebase!

I hope the article was useful. If you have any feedback or questions please feel free to reach out.

Thanks for reading!

Like what you read? Please share the article.

Avatar of Andrew Lord

WRITTEN BY

Andrew Lord

A software developer and tech leader from the UK. Writing articles that focus on all aspects of Android and iOS development using Kotlin and Swift.

Want to read more?

Here are some other articles you may enjoy.