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 anobject
with no dataPresentingState
brings with itContactsViewData
to render the contactsErrorState
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!
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.