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 2 of 4 of this article. Click here to view the first page.

Requesting the Characters

Start by replacing the first TODO in fetchCharacters() with the following:

lifecycleScope.launchWhenStarted {
  val response = apiService.getCharacters()
  val charactersResponseModel = response.body()
  if (response.isSuccessful){
    hideEmptyView()
    showCharacters(charactersResponseModel)
  }
}
Note: lifecycleScope throws an error when added, because it needs an imported class to work To fix this, just import the missing class.

This function has a couple of important components:

  • First, there’s lifecycleScope.launchWhenStarted{}, a CoroutineScope from the architecture components that’s lifecycle-aware. It launches a coroutine to perform operations on the background thread that let you make a network call using Retrofit. The project has the Retrofit part already set up for you, along with all its required classes and interfaces.
  • Inside the lifecycleScope, you make the network call. You use response, which calls getCharacters() from Retrofit’s ApiService class to get a list of characters charactersResponseModel derives its value from the response body of the network call.
  • Finally, you check if the response is successful and call showCharacters(charactersResponseModel).
Note: Take a look at the Coroutines documentation for more info about Coroutines.

Build and run and you’ll see the list of Rick and Morty characters:

App displaying Rick and Morty characters

Hurray, the app runs as expected. But there’s a problem: With this kind of approach, this is what happens when an error occurs:

Errors crushing your app

In classic state management, errors completely crash your app.

Addressing Errors

Your next thought might be to catch all errors with an else statement.

To try this, add the following catchall else statement for //TODO 2:

else {
 handleError("An error occurred")
}

To reproduce an error, navigate to data/network/ApiService.kt and make the following change to the @GET call:

@GET("/api/character/rrr")

Notice the addition at the end of the path. Your app won’t like that.

Now, build and run and the app will show errors:

An error occurred screenshot

Though you might think that else has rescued you, a closer look reveals some errors that else doesn’t handle – and they crash the app.

The errors that you have not yet handled are HTTP errors, network exceptions and null responses from the API.

Your next step will be to handle these kinds of errors.

Using a Try-Catch Statement to Catch Errors

If else isn’t robust enough to catch all the errors, maybe a try-catch statement might do the trick? You’ll try that next.

For //TODO 3, modify the entire code in fetchCharacters with a try-catch. The code in the method should look as follows:

  lifecycleScope.launchWhenStarted {
    try {
      val response = apiService.getCharacters()
      val charactersResponseModel = response.body()
      if (response.isSuccessful) {
        hideEmptyView()
        showCharacters(charactersResponseModel)
      } else {
        handleError("An error occurred")       
      }
      
    } catch (error: IOException) {
      showEmptyView()
      handleError(error.message!!)
    }
  }
Note: IOException throws an error when added, just import the missing class to fix this.

Here, the try-catch makes sure the app no longer crashes. It displays an error instead.

Let’s try this out. Make sure your device has no internet connection, then build and run the app. Swipe down to begin requesting characters, shortly, you’ll see the following:

Unable to resolve host error

The app no longer crashes when there’s no internet connection. Instead, it displays the Unable to resolve host error message.

However, you have only addressed one type of error, making it hard to know what’s gone wrong, especially for the case of HTTP errors. You’ll address that problem in the next section.

Handling HTTP Errors

Continue testing your states and and you’ll realize that there are several different HTTP errors that help you know what the problem is.

With the current approach, you won’t see what’s causing the problem because you’ll only get the generic error message: “An error occurred”.

Catch more errors

To catch more of the HTTP errors, replace the code within the else branch in fetchCharacters with this:

 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!!")
 }

The code block has a when statement, which takes response.code() from the network call as a parameter. It has some specific cases – 403, 404, 500, 502, 301 and 302 – plus the default else, in case the code isn’t specified. For each case, you handle the error with the appropriate message for each HTTP code.

Navigate to data/network/ApiService.kt again and change the end of the @GET call:

@GET("/api/character/rrr")

Notice the addition at the end of the path. Again, your app won’t like that.

Build and run and you’ll see an Internal server error message instead of the generic error message:

Internal Server Error screenshot

Congratulations, you can now catch HTTP errors and show the user what exactly went wrong instead of a generic message.

Now that you’ve handled your error message nicely, your next step will be to make the download experience more transparent to the user.

Note: Make sure you go back and undo the change on ApiService.kt to remove the addition that causes errors.

Indicating Download Progress

It’s not a great experience to click on an option to download data and get no response until the network call finishes. To make your app more user-friendly, your next step is to show a progress dialog so that users can know that the app is fetching data.

You’ll now address //TODO 5 by adding showRefreshDialog() below fetchCharacters() inside OnViewCreated. Your code should look like the following:

  lifecycleScope.launchWhenStarted {
    try {
      val response = apiService.getCharacters()
      val charactersResponseModel = response.body()
      if (response.isSuccessful) {
        hideEmptyView()
        showCharacters(charactersResponseModel)
      } 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!!)
    }
  }

  showRefreshDialog()

Build and run to see the loading icon.

The app displays a loading icon

You’re almost done, but you have one more case to handle before your app is ready to go.