🚊 Railway-oriented library to easily model and handle success/failure for Kotlin, Android, and Retrofit.
🚊 Railway-oriented library to model and handle success/failure easily for Kotlin, Android, and Retrofit.
Railway Oriented Programming is a functional approach to handling success and errors in normalized ways, always allowing you to predict the result. This library helps you to implement Railway-Oriented models and functions in Kotlin and Android (especially with Retrofit). Read Railway Oriented Programming if you want to learn more about ROP.
You'll find the use cases in the repositories below:
This library provides a normalized result model, Result
, representing the success or error of any business work.
Add the dependency below to your module's build.gradle
file:
dependencies {
implementation("io.getstream:stream-result:$version")
}
This is a basic model to represent a normalized result from business work. This looks similar to Kotlin's Result, but Stream Result was designed to include more information about success and error and support more convenient functionalities to handle results. Result is basically consist of two detailed types below:
value
property, a generic type of Result.value
property, the Error
type.You can simply create each instance of Result
like the example below:
val result0: Result<String> = Result.Success(value = "result")
val result1: Result<String> = Result.Failure(
value = Error.GenericError(message = "failure")
)
val result = result0 then { result1 }
result.onSuccess {
..
}.onError {
..
}
Result.Failure
has Error
as a value property, which contains error details of your business work. Basically, Error
consists of three different types of errors below:
You can create each instance like the example below:
val error: Error = Error.GenericError(message = "error")
try {
..
} catch (e: Exception) {
val error: Error = Error.ThrowableError(
message = e.localizedMessage ?: e.stackTraceToString(),
cause = e
)
}
val error: Error = Error.NetworkError(
message = "error",
serverErrorCode = code,
statusCode = statusCode
)
Stream Result library useful extensions below to effectively achieve Railway Oriented Programming in Kotlin:
Composition the Result
with a given Result
from a lambda function.
val result0: Result<String> = Result.Success(value = "result0")
val result1: Result<Int> = Result.Success(value = 123)
val result = result0 then { result1 }
result.onSuccess { intValue -> .. }
Returns a transformed Result
of applying the given function if the Result
contains a successful data payload.
val result: Result<String> = Result.Success(value = "result")
val mappedResult = result.map { 123 }
mappedResult.onSuccess { intValue -> }
Returns a transformed Result
from results of the function if the Result
contains a successful data payload. Returns an original Result
if the Result
contains an error payload.
val result: Result<String> = Result.Success(value = "result")
val mappedResult = result.flatMap { Result.Success(value = 123) }
mappedResult.onSuccess { intValue -> }
Stream Result library provides retrofit call integration functionalities to help you to construct a Result
model easily from the network requests on Android with the same approaches of Railway Oriented Programming.
Add the dependency below into your module's build.gradle
file:
dependencies {
implementation("io.getstream:stream-result-call-retrofit:$version")
}
You can return RetrofitCall
class as a return type on your Retrofit services by adding RetrofitCallAdapterFactory
on your Retrofit.Builder
like the example below:
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(..)
.addCallAdapterFactory(RetrofitCallAdapterFactory.create())
.build()
val posterService: PosterService = retrofit.create()
interface PosterService {
@GET("DisneyPosters2.json")
fun fetchPosterList(): RetrofitCall<List<Poster>>
}
RetrofitCall
class allows you to execute network requests easily like the example below:
interface PosterService {
@GET("DisneyPosters2.json")
fun fetchPosterList(): RetrofitCall<List<Poster>>
}
val posterService: PosterService = retrofit.create()
// Execute a network request asynchronously with a given callback.
posterService.fetchPosterList().enqueue { posters ->
..
}
// Execute a network request in a coroutine scope.
// If you use coroutines, we'd recommend you to use this way.
viewModelScope.launch {
val result = posterService.fetchPosterList().await()
result.onSuccess {
..
}.onError {
..
}
}
RetrofitCall
provides useful extensions for sequential works following Railway Oriented Programming approaches.
Run the given function
before running a network request.
val result = posterService.fetchPosterList()
.doOnStart(viewModelScope) {
// do something..
}.await()
Run the given function
before running a network request.
val result = posterService.fetchPosterList()
.doOnStart(viewModelScope) {
// do something before running the call..
}
.doOnResult(viewModelScope) {
// do something after running the call..
}.await()
Maps a Call
type to a transformed Call
.
val result = posterService.fetchPosterList()
.map { it.first() }
.await()
So you can chain all the extensions sequentially like the example below:
val result = posterService.fetchPosterList()
// retry if the network request fails.
.retry(viewModelScope, retryPolicy)
// do something before running the network request.
.doOnStart(viewModelScope) {
// do something..
}
// do something after running the network request.
.doOnResult(viewModelScope) {
// do something..
}
// map the type of call.
.map { it.first() }
.await()
result.onSuccess {
// do something..
}.onError {
// do something..
}
Retry a network request following your RetryPolicy
.
private val retryPolicy = object : RetryPolicy {
override fun shouldRetry(attempt: Int, error: Error): Boolean = attempt <= 3
override fun retryTimeout(attempt: Int, error: Error): Int = 3000
}
val result = posterService.fetchPosterList()
// retry if the network request fails.
.retry(viewModelScope, retryPolicy)
.await()
You can customize the creating of Error
from an error response according to your backend service by implementing your ErrorParser
class. You can provide your custom ErrorParser
to RetrofitCallAdapterFactory
. If not, it will use a default ErrorParser
, which uses Kotlin Serialization to decode json formats.
internal class MyErrorParser : ErrorParser<DefaultErrorResponse> {
@Suppress("UNCHECKED_CAST")
override fun <T : Any> fromJson(raw: String): T {
// use moshi or something that you can serialize from json response.
}
override fun toError(okHttpResponse: Response): Error {
// build Error with a given okHttpResponse.
}
override fun toError(errorResponseBody: ResponseBody): Error {
// build Error with a given errorResponseBody.
}
}
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(..)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RetrofitCallAdapterFactory.create(
errorParser = MyErrorParser()
))
.build()
Support it by joining stargazers for this repository. :star:
Also, contributors on GitHub for my next creations! 🤩
Copyright 2023 Stream.IO, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.