We have chosen to stop support and improvements on Esito since we internally migrated to Arrow. When Esito was created (at the beginning of 2021), Arrow was still in progress and was not stable at all (version 1.0.0 was released in September 2021). Basically, using Arrow, you can achieve the same results and also explore functional programming more, if you like!
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.
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation("com.github.Subito-it.Esito:core:(insert latest version)")
}
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.
Esito result has several operators, such as map
and flatmap
, for examples see Recipes.
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.
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.
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.