Kotlin and Android: Beyond the Basics with Sealed Classes

In this tutorial, you’ll learn about Kotlin sealed classes and how to use them to manage states when developing Android apps. By Harun Wangereka.

4.6 (19) · 1 Review

Download materials
Save for later
Share

Managing UI state is one of the most important things you can do as an app developer. Done wrong, state management causes problems like poor error handling, hard-to-read code and a lack of separation between UI and business logic.

With Kotlin, sealed classes make it simpler to manage state.

In this tutorial, you’ll learn the advantages of Kotlin sealed classes and how to leverage them to manage states in your Android apps.

You’ll do this by building the RickyCharacters app, which displays a list of characters from the television show. You’ll download the character data from The Rick And Morty API and manage states in the app by using the Retrofit library to make network calls.

If you’re unfamiliar with Kotlin, take a look at our Kotlin For Android: An Introduction tutorial.

To brush up on your Android skills, check out our Android and Kotlin tutorials.

You can also learn more about sealed classes in our Kotlin Sealed Classes tutorial.

Note: This tutorial assumes you have experience with developing for Android in Kotlin and know the basics of sealed classes.

Getting Started

Download the begin project by clicking the Download Materials button at the top or bottom of the tutorial.

Extract the zip file and open the begin project in Android Studio by selecting Open an existing Android Studio project from the welcome screen. Navigate to the begin directory and click Open.

Once the Gradle sync finishes, build and run the project using the emulator or a device. You’ll see this screen appear:

Home screen

You probably expected some Rick and Morty Characters on the home screen, but that feature isn’t ready yet. You’ll add the logic to fetch the character images later in this tutorial.

Advantages of Sealed Classes

Sealed classes are a more powerful extension of enums. Here are some of their most powerful features.

Multiple Instances

While an enum constant exists only as a single instance, a subclass of a sealed class can have multiple instances. That allows objects from sealed classes to contain state.

Look at the following example:

sealed class Months {
    class January(var shortHand: String) : Months()
    class February(var number: Int) : Months()
    class March(var shortHand: String, var number: Int) : Months()
}

Now you can create two different instances of February. For example, you can pass the 2019 as an argument to first instance, and 2020 to second instance, and compare them.

Inheritance

You can’t inherit from enum classes, but you can from sealed classes.

Here’s an example:

sealed class Result {
    data class Success(val data : List<String>) : Result()
    data class Failure(val exception : String) : Result()
}

Both Success and Failure inherit from the Result sealed class in the code above.

Kotlin 1.1 added the possibility for data classes to extend other classes, including sealed classes.

Architecture Compatibility

Sealed classes are compatible with commonly-used app architectures, including:

  • MVI
  • MVVM
  • Redux
  • Repository pattern

This ensures that you don’t have to change your existing app architecture to leverage the advantages of sealed classes.

“When” Expressions

Kotlin lets you use when expressions with your sealed classes. When you use these with the Result sealed class, you can parse a result based on whether it was a success or failure.

Here’s how this might look:

when (result) {
  is Result.Success -> showItems(result.data)
  is Result.Failure -> showError(result.exception)
}

This has a few advantages. First, you can check the type of result using Kotlin’s is operator. By checking the type, Kotlin can smart-cast the value of result for you for each case.

If the result is a success, you can access the value as if it’s Result.Success. Now, you can pass items without any typecasting to showItems(data: List).

If the result was a failure, you display an error to the user.

Another way you can use the when expression is to exhaust all the possibilities for the Result sealed class type.

Typically, a when expression must have an else clause. In the example above, however, there are no other possible types for Result. The compiler and IDE know you don’t need an else clause.

Look what happens when you add an InvalidData object to the sealed class:

sealed class Result {
    data class Success(val data : List<String>) : Result()
    data class Failure(val exception : String) : Result()
    object InvalidData : Result()
}

The IDE now shows an error on the when statement because you haven’t handled the InvalidData branch of Result.

Error message: When expression must be exhaustive

The IDE knows you didn’t cover all your cases here. It even knows which possible types result could be, depending on the Result sealed class. The IDE offers you a quick fix to add the missing branches.

IDE offers a fix for the When statement error

Note: You only make a when expression exhaustive if you use it as an expression, you use its return type or you call a function on it.

Managing State in Android

Fasten your seat belt, as you’re about to travel back in time to a multiverse where sealed classes did not exist. Get ready to see how Rick and Morty survived back then.

Classical State Management

Your goal is to fetch a list of characters for the “Rick and Morty” television show from The Rick And Morty API and display them in a Recycler view.

Open CharactersFragment.kt in com.raywenderlich.android.rickycharacters.ui.views.fragments.classicalway and you’ll see the following code:

class CharactersFragment : Fragment(R.layout.fragment_characters) {
  // 1
  private val apiService = ApiClient().getClient().create(ApiService::class.java)
  private lateinit var charactersAdapter: CharactersAdapter

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 2
    charactersAdapter = CharactersAdapter {character ->
      displayCharacterDetails(character)
    }
    recyclerViewMovies.adapter = charactersAdapter
    fetchCharacters()
    swipeContainer.setOnRefreshListener {
      fetchCharacters()
    }
  }

  // 3
  private fun displayCharacterDetails(character: Character) {
    val characterFragmentAction =
        CharactersFragmentDirections.actionCharactersFragmentToCharacterDetailsFragment(
            character)
    findNavController().navigate(characterFragmentAction)
  }

To explain the code above:

  1. You have two variables at the very top. One represents the retrofit service class and the other represents the recyclerview adapter class.
  2. Inside onViewCreated, you initialize the adapter class and set the adapter for the Recycler view, which displays the list of characters. There’s a call to fetchCharacters and you also set a refresh listener to the SwipeRefeshLayout that calls the fetchCharacters on refresh.
  3. displayCharacterDetails(character: Character) is responsible for navigating to the CharacterDetailsFragment and showing character details once you tap on each character.

Below displayCharactersDetails(character: Characters), there’s more code responsible for requesting the characters and displaying the appropriate UI.

// 1
private fun fetchCharacters() {
    //TODO 1 Make a get characters Request

    //TODO 2 Catch errors with else statement

    //TODO 3 Catch errors with try-catch statement

    //TODO 4 Catch HTTP error codes

    //TODO 5 Add refresh dialog

    //TODO 6 Handle null response body
  }

  // 2
  private fun showCharacters(charactersResponseModel: CharactersResponseModel?) {
    charactersAdapter.updateList(charactersResponseModel!!.results)
  }

  // 3
  private fun handleError(message : String) {
    errorMessageText.text = message
  }
 
  // 4
  private fun showEmptyView() {
    emptyViewLinear.show()
    recyclerViewMovies.hide()
    hideRefreshDialog()
  }

  // 5
  private fun hideEmptyView() {
    emptyViewLinear.hide()
    recyclerViewMovies.show()
    hideRefreshDialog()
  }

  // 6
  private fun showRefreshDialog() {
    swipeContainer.isRefreshing = true
  }
  
  // 7
  private fun hideRefreshDialog() {
    swipeContainer.isRefreshing = false
  }

Here’s an explanation for the code above:

  1. fetchCharacters() is responsible for fetching the characters from the api and handling the response. There’s a couple of //TODOs here, which you’ll address one by one in this tutorial.
  2. showCharacters(charactersResponseModel: CharactersResponseModel?) takes CharacterResponseModel as an argument. This is a data class representing the response from the Rick and Morty API. The function sends the list of characters to the CharactersAdapter.
  3. handleError(message : String) takes a String as an argument and sets the message to a TextView.
  4. showEmptyView() hides the Recycler view and shows the empty view. This is a linear layout with an image and an error text view for displaying the error message. Notice recyclerViewMovies.hide() uses an extension function from com.raywenderlich.android.rickycharacters.utils.extensions.kt. This is also where you find the show() extension function.
  5. hideEmptyView() hides the empty view and shows the Recycler view.
  6. showRefreshDialog() sets the refresh property of SwipeRefreshLayout to true.
  7. hideRefreshDialog sets the refresh property of SwipeRefreshLayout to false.

With the code in this class explained, we’re ready to begin coding. In the next section, you’ll add begin to request characters from the Rick and Morty API. Time to get schwifty!