Kotlin sealed class

Photo by Andrew Neel on Unsplash

Kotlin sealed class

Introduction

A sealed class is a one-of-a-kind combination of an enum and an abstract class. Our lesson will begin with the creation of an enum class. We will be forced to use an abstract class as it becomes more sophisticated.

The sealed class will then take over, showing to be more strong and useful.

Prerequisites

  • A basic understanding of the Kotlin programming language.

  • A basic understanding of object-oriented programming concepts.

  • IntelliJ IDEA is preferred. If unavailable, the Kotlin Playground can be used.

Enum Class

'Enum' is an abbreviation for 'Enumerated type.' The term 'enumerated type' is derived from the English word 'enumerate.' To enumerate something is to list it one by one. In programming, an enumerated type is a type that consists of a list of elements with the same type as the enumerated type.

Open main.kt

Add the following code:

enum class LoadState{
    SUCCESS,
    LOADING,
    ERROR,
    IDLE
}

You have created an enum class that can keep track of the load state.

Create a function getStateOutput(loadState:LoadState) next to fun main(). It will print a different string depending on the load state.

fun getStateOutput(loadState:LoadState){
    return when (loadState){

    }
}

Use the project quick fix to add the remaining branches.

The project quick fix is only available in IntelliJ IDEA. To access it click on the red-underlined when keyword. Press Alt+Enter in Windows or Option+Enter in Mac. Alternatively, you can click on the lightbulb that pops up.

Select add remaining branches

Add the following strings to the branches.

fun getStateOutput(loadState:LoadState){
    return when(loadState){
        LoadState.SUCCESS -> {
            println("Successfully loaded data")
        }
        LoadState.LOADING -> {
            println("Still loading...")
        }
        LoadState.ERROR -> {
            println("ERROR")
        }
        LoadState.IDLE -> {
            println("IDLE")
        }
    }
}

Create a repository singleton that will mimic the fetching of data. Create the singleton using the object keyword as shown:

object Repository{
    private var loadState:LoadState = LoadState.IDLE
    //Data to be fetched when we startFetch()
    private var dataFetched:String? = null
    fun startFetch(){
        loadState = LoadState.LOADING
        dataFetched = "data"
    }
    fun finishFetch(){
        loadState = LoadState.SUCCESS
        //Return data fetched to its original state
        dataFetched = null
    }
    fun errorFetch(){
        loadState = LoadState.ERROR
    }
    fun getLoadState(): LoadState {
        return loadState
    }
}

Now play around with these methods in fun main():

fun main(){
    Repository.startFetch()
    getStateOutput(Repository.getLoadState())
    Repository.finishFetch()
    getStateOutput(Repository.getLoadState())
    Repository.errorFetch()
    getStateOutput(Repository.getLoadState())
}

output:

    Still loading...

    Successfully loaded data

    ERROR

Enum classes are clearly handy for dealing with state. They are quite good at keeping track of things.

Consider the following situation:

If you wanted to produce a different success message based on the data retrieved? You may also wish to look for unusual exceptions in mistakes.

To implement the above functionality:

  • SUCCESS will have to emit a unique string

  • ERROR will have to emit a unique exception

  • LOADING and IDLE remain generic.

Using enum classes, we would have to do this:

enum class LoadState{
    SUCCESS(val data:String),
    LOADING,
    ERROR(val exception:Exception),
    NOTLOADING
}

The code above produces an error. This is because you can not represent constants differently in an enum class.

To solve this problem, you can inherit from an abstract class. This allows you to represent states differently.

Abstract class

An abstract class is one that has not yet had its functionality implemented. It can be used to construct things that follow its protocol.

Using an abstract class for state management

Replace the enum class LoadState with the code below:

abstract class LoadState

data class Success(val dataFetched:String?):LoadState()
data class Error(val exception: Exception):LoadState()
object NotLoading:LoadState()
object Loading:LoadState()

Success can now emit a unique string dataFetched. Error can now emit a unique exception exception. All the states conform to the type LoadState. However, they are very different from each other.

Replace the code in fun getStateOutput with this:

fun getStateOutput(loadState:LoadState){
    return when(loadState){
        is Error-> {
            println(loadState.exception.toString())
        }
        is Success -> {
            //If the dataFetched is null, return a default string.
            println(loadState.dataFetched?:"Ensure you startFetch first")
        }
        is Loading-> {
            println("Still loading...")
        }
        is NotLoading -> {
            println("IDLE")
        }
        //you have to add an else branch because the compiler cannot know whether the abstract class is exhausted.
        else-> println("invalid")
    }
}

Replace the code in the Repository with this:

object Repository {
    private var loadState:LoadState = NotLoading
    private var dataFetched:String? = null

    fun startFetch(){
        loadState = Loading
        dataFetched = "Data"
    }
    fun finishFetch(){
        //passing the dataFetched to Success state.
        loadState = Success(dataFetched)
        dataFetched = null
    }
    fun errorFetch(){
        //passing a mock exception to the loadstate.
        loadState = Error(Exception("Exception"))
    }
    fun getLoadState(): LoadState {
        return loadState
    }
}

Run fun main() again.

output:

    Still loading...
    Data
    java.lang.Exception: Exception

You gain the freedom to represent your states differently by extending an abstract class rather than utilizing enums.

Unfortunately, you misplaced something important along the road. The limited set of enum types. The kotlin compiler is currently in trouble. It cannot determine whether the branches of the when statement were comprehensive. That's why you needed to include else as a branch.

This is when the sealed classes come into play. They give you the finest of both worlds. You gain the option to represent your states differently, as well as the limitations that come with enums.

Sealed class

A sealed class is a restricted class hierarchy abstract class. Classes that inherit from it must be located in the same directory as the sealed class.

This gives you more control over your heirs. They are limited, but they also provide for latitude in state representation.

Using sealed classes for state management

In your code, replace the abstract keyword in abstract class LoadState with sealed.

After that, head over to the else branch in fun getStateOutput().

IntelliJ IDEA has the following error:

    'when' is exhaustive so 'else' is redundant here

This is because a sealed class is restricted. The compiler can tell when all the branches in the when statement have been listed. This means you can safely remove the redundant else branch.

Utilizing the full power of sealed classes

Sealed classes can nest data classes, classes, objects, and also other sealed classes. The autocomplete feature shines when dealing with other sealed classes. This is because the IDE can detect the branches within these classes.

In your sealed class LoadState, replace the data class Error with this sealed class:

sealed class Error:LoadState(){
    data class CustomIOException(val ioException: IOException):Error()
    data class CustomNPEException(val npeException:NullPointerException):Error()
}

In the fun getStateOutput() delete the is Error branch and allow the IDE to fill the remaining branches.

The final code of the function will look like this:

fun getStateOutput(loadState:LoadState){
    return when(loadState){
        is Success -> {
            //If there dataFetched is null, return this default string.
            println(loadState.dataFetched?:"Ensure you startFetch first")
        }
        is Loading-> {
            println("Still loading...")
        }
        is NotLoading -> {
            println("IDLE")
        }
        is Error.CustomIOException -> {
            println(loadState.ioException.toString())
        }
        is Error.CustomNPEException -> {
            println(loadState.npeException.toString())
        }
    }
}

In the Repository, delete the function fun errorFetch().

Add the following attributes:

    private val npeException = NullPointerException("There was a null pointer exception")
    private val ioException = IOException("There was an IO exception")

The attributes are exceptions that we will pretend to catch.

Add these functions to Repository:

    fun ioErrorFetchingData(){
        loadState = Error.CustomIOException(ioException)
    }
    fun npeErrorFetchingData(){
        loadState = Error.CustomNPEException(npeException)
    }
In fun *main()* replace *Repository.errorFetch* with the following code:

    Repository.ioErrorFetchingData()
    getStateOutput(Repository.getLoadState())
    Repository.npeErrorFetchingData()
    getStateOutput(Repository.getLoadState())

After running, the output will look like:

    Still loading...
    Data
    java.io.IOException: There was an IO exception
    java.lang.NullPointerException: There was a null pointer exception

Conclusion

You began with enum classes. You saw how to take advantage of their limitation when dealing with state.

Then you ran across an issue with enum classes. They lack the freedom to represent your states in a variety of ways. You overcame this problem by adding abstract classes.

Finally, you realized you had misplaced something vital. The limitation of enum classes. You regained control by replacing the abstract class with a sealed class.