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
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Handling Null Responses

As you finish managing state the classic way, you need to handle what happens when the success response is null. Handling this case is your //TODO 6.

To address this, add a null check on the on successful response by replacing if (response.isSuccessful) {} with this:

if (response.body() != null) {
  hideEmptyView()
  showCharacters(charactersResponseModel)
} else {
  showEmptyView()
  handleError("No characters found")
}

In the code above, you check if response.body() is null. If it’s not, hide the empty view and show the characters. If it is, you handle the error with a “No characters found” message.

The final results looks like this:

private fun fetchCharacters() {
  lifecycleScope.launchWhenStarted {
    try {
      val response = apiService.getCharacters()
      val charactersResponseModel = response.body()
      if (response.isSuccessful) {
        if (response.body() != null) {
          hideEmptyView()
          showCharacters(charactersResponseModel)
        } else {
          showEmptyView()
          handleError("No characters found")
        }

      } else {
        showEmptyView()
        when (response.code()) {
          403 -> handleError("Access to resource is forbidden")
          404 -> handleError("Resource not found")
          500 -> handleError("Internal server error")
          502 -> handleError("Bad Gateway")
          301 -> handleError("Resource has been removed permanently")
          302 -> handleError("Resource moved, but has been found")
          else -> handleError("All cases have not been covered!!")
        }
      }
    } catch (error : IOException) {
      showEmptyView()
      handleError(error.message!!)
    }
  }

Problems With Classical State Management

As you went implementing state using the classical approach, you may have noticed some problems with it. Including:

  • Poor error handling.
  • Hard-to-read code.
  • No separation of concerns.
  • Small changes are hard to implement.
  • Mixes business logic with the UI. The fetchCharacters function is handling network responses as well as UI for instance
  • Unpredictable UI states.

Luckily, Kotlin gives you a better way to handle states.

Simplifying States With Sealed Classes

Sealed classes can eliminate the problems associated with the old way of managing state.

Modeling States With Sealed Classes

With sealed classes, you think about all the possible states before you start to code. You then keep the end result in mind when you start to code.

In this example, you need to follow these states:

  • Success with a list of characters.
  • Invalid data – no characters found.
  • Generic error state.
  • Network exceptions – errors caused by network failure.
  • HTTP errors that represent HTTP error codes. There can be more than one.

So with these states in mind, navigate to data ▸ states. Right-click and create a new Kotlin class named NetworkState. Then add the following code:

sealed class NetworkState {
  data class Success(val data : CharactersResponseModel) : NetworkState()
  object InvalidData : NetworkState()
  data class Error(val error : String) : NetworkState()
  data class NetworkException(val error : String) : NetworkState()
  sealed class HttpErrors : NetworkState() {
    data class ResourceForbidden(val exception: String) : HttpErrors()
    data class ResourceNotFound(val exception: String) : HttpErrors()
    data class InternalServerError(val exception: String) : HttpErrors()
    data class BadGateWay(val exception: String) : HttpErrors()
    data class ResourceRemoved(val exception: String) : HttpErrors()
    data class RemovedResourceFound(val exception: String) : HttpErrors()
  }
}

The types in the sealed class are data classes, objects and another sealed class representing the HTTP error states.

Now that you have all the states ready, you’re ready to apply the states to your network call.

Applying the States to Your App

Navigate to ui ▸ views ▸ fragments ▸ sealedclassway. Open StateCharactersFragment.kt Replace getCharacters with the following:

private fun getCharacters() {
  lifecycleScope.launchWhenStarted {
    showRefreshDialog()
    val charactersResult = fetchCharacters()
    handleCharactersResult(charactersResult)
  }
}

Here you’re setting up the Fragment to show it’s in a loading state, via showRefreshDialog, whilst the app makes a network request through fetchCharacters(). You handle the result of the request through handleCharactersResult.

Next, you need to write the fetchCharacters method to request the characters from the API. Update the fetchCharacters like so:

private suspend fun fetchCharacters() : NetworkState {
    return try {
      val response = apiService.getCharacters()
      if (response.isSuccessful) {
        if (response != null) {
          NetworkState.Success(response.body()!!)
        } else {
          NetworkState.InvalidData
        }
      } else {
        when(response.code()) {
          403 -> NetworkState.HttpErrors.ResourceForbidden(response.message())
          404 -> NetworkState.HttpErrors.ResourceNotFound(response.message())
          500 -> NetworkState.HttpErrors.InternalServerError(response.message())
          502 -> NetworkState.HttpErrors.BadGateWay(response.message())
          301 -> NetworkState.HttpErrors.ResourceRemoved(response.message())
          302 -> NetworkState.HttpErrors.RemovedResourceFound(response.message())
          else -> NetworkState.Error(response.message())
        }
      }

    } catch (error : IOException) {
      NetworkState.NetworkException(error.message!!)
    }
}

After adding this, make sure to import the NetworkState and IOException classes.

There are a few differences here from the classical approach. The function has a suspend keyword, which means it can pause and resume later when you have a response from the server.

All the condition checks are the same as before, but now instead of handling the results, you are assigning variables to NetworkState depending on the response.

For a success response, you check if the response is null or not. If it’s not, you set the state as NetworkState.Success(response.body()!!).

Notice the success state takes the response body and sets the state to NetworkState.InvalidData if its null. If the response is not successful, you handle the error along with the HTTP errors.

For HTTP errors, you set the state to NetworkState.HttpErrors, depending on the error code. For normal errors, you set the state to NetworkState.Error(response.message()). In the catch block, you set the state to NetworkState.NetworkException(error.message!!).

Notice all these states have variables that can have more than one value, which is one of the advantages of sealed states.

Also, this function only deals with fetching data and updating the states, it contains no business logic or UI logic. This helps the method be focused on doing one thing and makes the code much more readable.

To display the results, add the following function to the fragment just below fetchCharacters():

private fun handleCharactersResult(networkState: NetworkState) {
    return when(networkState) {
      is NetworkState.Success -> showCharacters(networkState.data)
      is NetworkState.HttpErrors.ResourceForbidden -> handleError(networkState.exception)
      is NetworkState.HttpErrors.ResourceNotFound -> handleError(networkState.exception)
      is NetworkState.HttpErrors.InternalServerError -> handleError(networkState.exception)
      is NetworkState.HttpErrors.BadGateWay -> handleError(networkState.exception)
      is NetworkState.HttpErrors.ResourceRemoved -> handleError(networkState.exception)
      is NetworkState.HttpErrors.RemovedResourceFound -> handleError(networkState.exception)
      is NetworkState.InvalidData -> showEmptyView()
      is NetworkState.Error -> handleError(networkState.error)
      is NetworkState.NetworkException -> handleError(networkState.error)
    }
}

This function takes NetworkState as an argument and uses a when expression as a return statement to give the exhaustive advantage. Since each state exists independent of the others, you handle all the possible states.

Displaying the Character Details

Now you’ve converted the network call code to use sealed classes, let’s hook up the UI. Since fetchCharacters is a suspend function call, we’ll need to wrap calls to it in a coroutine. In getCharacters wrap the calls to getCharacters() in lifecycle scope coroutine like so:

   
lifecycleScope.launchWhenStarted {
  hideEmptyView()
  showRefreshDialog()
  val charactersResult = getCharacters()
  handleCharactersResult(charactersResult)
}

Also in onViewCreated, add a lifecycleScope within the refresh listener:

swipeContainer.setOnRefreshListener {
  lifecycleScope.launchWhenStarted {
    getCharacters()
  }
}

Also add the lifecycle import when prompted.

Here, you call showRefreshDialog() to show the refresh dialog whilst the app fetches the characters from the api. characterResult calls getCharacters() to make a network request to fetch the characters, then the next line calls handleCharactersResult(charactersResult) with the characterResult variable.

Finally, we need to hook up our UI to use the new Fragment, as it’ll continue to use CharactersFragment instead of our shiny new StateCharactersFragment. Navigate to res ▸ navigation and open main_nav_graph.xml.

Showing the navigation graph

Choose the Text tab and change startDestination at the navigation tags with stateCharactersFragment.

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_nav_graph"
    app:startDestination="@id/stateCharactersFragment">

Now, build and run.

Behold, the app runs well, all states properly managed. The character list displays properly:

App displaying Rick and Morty characters

The character detail screen works, as well:

Character Details

Congratulations! You’ve completed your app. In the process, you learned about the features and advantages of sealed classes and how to use them in Android to tame states.