Esito
Coroutines are great for Asynchronous and non-blocking programming, but exceptions could be hard to handle, especially in a coroutine. [1, 2, 3]
Exceptions in a coroutine could cancel their parents, and each canceled parent cancels all its children, and this is not always the desired behavior.
While this could be changed using a SupervisorJob
, it's still easy to shoot yourself in the foots throwing exceptions in Coroutines, so Esito is proposing an alternative approach making explicit the computations that could fail.
Installation
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation("com.github.Subito-it.Esito:core:(insert latest version)")
}
Getting started
The main class is the sealed class Result
, with two subtypes:
Success
: it contains the result of a successful computationFailure
: it contains the cause of an unsuccessful computation
The goal when using Esito is to avoid throwing exceptions and use Result as the return type of a function that can fail:
sealed class ConversionError
object EmptyInput : ConversionError()
object NotNumericInput : ConversionError()
fun fromStringToInt(input: String): Result<Int, ConversionError> = when {
input.isBlank() -> Result.failure(EmptyInput)
else -> runCatching { input.toInt() }.mapError { NotNumericInput }
}
In this example we are defining all the possible failures in ConversionError
, then we are applying some logic to build our result.
If the input is not blank we are using the runCatching
method to wrap a method throwing an exception and mapping the eventual error in our desired type.
Operators
Esito result has several operators, such as map
and flatmap
, for examples see Recipes.
Retrofit Integration
Esito ships an integration with retrofit, after a one-line setup you can start to use Result return type in your Retrofit interfaces:
interface GitHubService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String?): Result<List<Repo>, Throwable>
}
For additional info and to learn how to use your own Error instead of Throwable have a look at this documentation.
Async Utilities
Esito offers some utilities for suspending methods returning Result. For example, suppose we have the following functions:
suspend fun getUserFullName(userId: Int): Result<String, FeatureError>
suspend fun getUserStatus(userId: Int): Result<UserStatus, FeatureError>
And we have to return an instance of DataForUI running in parallel getUserFullName
and getUserStatus
.
data class DataForUI(
val userFullName: String,
val userStatus: UserStatus
)
Esito exposes a zip
method to compute two independent execution in parallel:
suspend fun fetchIfo(userId: Int): Result<DataForUI, FeatureError> =
zip(
{ getUserFullName(userId) },
{ getUserStatus(userId) },
::DataForUI //syntactic sugar for constructor
).invoke()
For additional info have a look at this documentation.
Testing
Esito is providing two extension methods to facilitate testing: assertIsSuccess
and assertIsFailure
, here is an example of usage:
val success = Result.success<Int, Throwable>(42)
success.assertIsSuccess {
assertEquals(42, value)
}
val failure = Result.failure<Int, RuntimeException>(RuntimeException("Ops"))
failure.assertIsFailure {
assertEquals("Ops", error.message)
}
For additional info have a look at this documentation.