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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin and Android: Beyond the Basics with Sealed Classes
25 mins
- Getting Started
- Advantages of Sealed Classes
- Multiple Instances
- Inheritance
- Architecture Compatibility
- “When” Expressions
- Managing State in Android
- Classical State Management
- Requesting the Characters
- Addressing Errors
- Using a Try-Catch Statement to Catch Errors
- Handling HTTP Errors
- Indicating Download Progress
- Handling Null Responses
- Problems With Classical State Management
- Simplifying States With Sealed Classes
- Modeling States With Sealed Classes
- Applying the States to Your App
- Displaying the Character Details
- Where to Go From Here?
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)
}
}
This function has a couple of important components:
- First, there’s
lifecycleScope.launchWhenStarted{}
, aCoroutineScope
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 useresponse
, which callsgetCharacters()
from Retrofit’s ApiService class to get a list of characterscharactersResponseModel
derives its value from the response body of the network call. - Finally, you check if the response is successful and call
showCharacters(charactersResponseModel)
.
Build and run and you’ll see the list of 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:
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:
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!!)
}
}
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:
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”.
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:
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.
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.
You’re almost done, but you have one more case to handle before your app is ready to go.