The Magic of Kotlin Result Class

Ranbir Singh
5 min readDec 28, 2023

--

Code readability is one of the most crucial aspects when you’re coding. A significant part of achieving readability is in how you name objects, classes, and variables. It should be straightforward, even if it means using an extra word. Don’t worry; adding extra information to variable names won’t cause a chaotic mess in your codebase.

As we all know, computers understand only two things: 1 and 0, or On and Off. Similarly, our human minds work quite similarly. It’s easier for us to understand something by naming it true or false. This helps our brains easily remember or process that information. For instance, think back to the simple true/false questions in exams from our childhood. It was simpler to read the statement and give the answer (for very simple kids’ questions only).

Let’s explore how Kotlin’s Result class can apply the same theory and make our code super readable.

In most cases, when we need to check if an operation in our code is successful or not, we use this function:

val number = 10
val result = if (number > 5) {
"Number is greater than 5"
} else {
"Number is not greater than 5"
}

This is a very basic example of an operation to check if a condition is true or false. However, in this case, we already know about the else part, which gives us an edge to write for the other case. It provides information to the user or developer that this will be the outcome for both scenarios.

What if we encounter a bit more complicated condition where we need to use other tools to handle the situation, like a try-catch block? We won’t delve really deep into the tryCatch block, but we’ll see how a try-catch block works.

val input = "abc"
val number = try {
input.toInt() // Try to parse the string to an integer
} catch (e: NumberFormatException) {
null // If an exception occurs, assign null to 'number'
}

val result = if (number != null) {
"Parsed number: $number"
} else {
"Could not parse input as a number"
}

println(result)

Let’s explore the above code: we have input, which is a string, and we’re trying to convert it to an integer, which isn’t possible. So, it will throw an exception that we’ll catch in the code’s catch block. Following that, we’ll display a message based on that exception. Essentially, the try-catch block is used to handle exceptions while performing operations so that we can manage them as desired.

Now that we’ve grasped when and how we use the try-catch block, in my opinion, writing a try-catch block every time we perform an operation isn’t good for readability. It breaks the principle of not repeating yourself.

Let’s improve the above code with a real-life example. Consider where we commonly use try-catch blocks in our code. Yes, you guessed it right — when performing API calls. For every API call, we utilize a try-catch block.

import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.request.*
import io.ktor.http.*

val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}

suspend fun performApiCall(): String? {
return try {
val response=httpClient.get("https://your-api-url.com/endpoint").body<String>()
response // Return response if successful
} catch (e: Exception) {
e.printStackTrace() // Handle or log the exception here
null // Return null in case of an error
}
}

As you can see, we’re using KtorClienthere, but whether it’s Retrofit or any other similar tool, the concept remains the same.

In the piece of code above, we create a Ktor client and make an API call using a try-catch block with the GET operation. Imagine writing 50 API calls in your business logic; even though it’ll function, the try-catch block will be repeated every time you write an API call. Let’s improve this.

We’ll use runCatching for this. Behind the scenes, runCatching uses a try-catch block, but it also adds another thing: the Result class. The R in my case is generic and represents String.

return runCatching {
httpClient.get("https://your-api-url.com/endpoint").body<String>()
}
public inline fun <R> runCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: Throwable) {
Result.failure(e)
}
}

Now, by using runCatching in our code, it already looks super clean. Let's utilize another function provided by the Result class to further enhance its cleanliness and readability.

With our result potentially being either Success or Failure, we can easily handle errors with a super clean approach. Just by replacing a few things, the magic of the Result class will make your code shine.

 suspend fun performApiCall(): Result<String> {
return runCatching {
httpClient.get("https://your-api-url.com/endpoint").body<String>()
}
}



class MyViewModel : ViewModel() {

fun fetchData() {
viewModelScope.launch {
val result = repository.performApiCall()

result.onSuccess {
// Handle success if needed
}.onFailure {
// Handle failure if needed
}

}
}
}

The Result class isn't limited to the examples mentioned above; it also provides many more use cases with different extension functions. Here are a few:


//For cases solely interested in success response, using getOrNull() function and handling errors manually:

viewModelScope.launch {
if (result.isSuccess) {
val data = result.getOrNull()
// handle data
} else {
// handle error
}
}


//Folding the response, a concise approach for clean and readable code:

viewModelScope.launch {
result.fold(onSuccess = { data ->
// handle data
}, onFailure = { error ->
// handle error
})
}

Link for the official documentation on Result class supported functions."

We still have to write ‘runCathing’ every time, but we can further improve this by using extensions. Below, we can supercharge our Ktor client, making our code superb. You can create all the extensions you need, such as GetResult,PutResult,DeleteResult...., to solve this problem. I have created a library named KtorBoost that will supercharge your code. KtorBoost extends the functionality of the result class, making the developer experience smoother. Of course, if you don’t want to install the library, simply copy all the extension functions, and you’re good to go. Link for the library: KtorBoost.

// create this extenstiion function in your code base 
suspend inline fun <reified R> HttpClient.getResult(
urlString: String,
builder: HttpRequestBuilder.() -> Unit = {}
): Result<R> = runCatching { get(urlString, builder).body() }
//use this extenstion function like this
suspend fun performApiCall(): Result<String> {
return httpClient.getResult<String>("sample_get_url")

}

class MyViewModel : ViewModel() {

fun fetchData() {
viewModelScope.launch {
val result = repository.performApiCall()

result.onSuccess {
// Handle success if needed
}.onFailure {
// Handle failure if needed
}

}
}
}

Update: By default, runCatching might catch a CancellationException, affecting coroutine executions. To prevent this, we need to write our own version of runCatching, to prevent those issues, KtorBoost uses a custom runCatching.

public suspend inline fun <R> runSafeSuspendCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (c: CancellationException) {
throw c
} catch (e: Throwable) {
Result.failure(e)
}
}

Thanks for reaching the end! Share your thoughts — I’d love your feedback.If you liked it don’t forget to hit 👏

You can reach me on LinkedIn,Github,Twitter

Happy Coding! 💪

--

--

Ranbir Singh

Android Team Lead @ex-Intree | Software Architect | Open Source Software Engineer. https://github.com/AndroidPoet