Ketro
Simple and sane Retrofit request library for Kotlin helps wrap responses and provides easy error handling that can be easily translated to custom exception objects for easy and proper handling. Ketro supports LiveData request and also Coroutines functionality. As well easily propagate errors to the parent fragment/activity or handle within the ViewModel without losing your sanity🔥. Ketro is highly flexible and is a good tool for clean response parsing and management https://smilecs.github.io/ketro/
Install / Use
/learn @smilecs/KetroREADME
ketro

Ketro is a Retrofit response wrapper written in Kotlin that can be used to easily wrap REST API response to LiveData and exception/error handling both from the retrofit calls all the way to displaying an error in the view. Ketro allows and encourages the addition of custom exceptions so errors can easily be grouped and managed with adequate actions and feedback to your app users.
Include Dependency
Ketro is hosted using JitPack, just add the below line to your root build.gradle file
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
Multi - module projects:
Ketro now supports multi-module projects, the Ketro modules such as Wrapper and ApiErrorHandler have been put into a separate package to allow you expose these in a domain layer without including the Ketro dependency so as to enable separation of concerns between the data, presentation and domain layer.
dependencies {
implementation 'com.github.smilecs:ketro:1.4'
}
Ketro Request methods
Ketro offers a selection of methods that wrap your retrofit calls and return a LiveData object with a wrapper that contains an exception object if the request was unsuccessful or as the user defines. These methods are:
doRequest() : LiveData<Wrapper<R>>executeRequest(liveData:MutableLiveData<Wrapper<R>>)suspend fun doRequest(): Wrapper<T>suspend fun execute(): KResponse<T>
Usage
Inorder to use these wrappers for your request, you must extend the ketro GenericRequestHandler<T> which takes in a class type which you would like to observe with livedata.
Ketro offers two request patterns:
GenericRequestHandler<T>Request<T>
1. GenericRequestHandler API
class LobbyRequest(private val mainType: String = "") : GenericRequestHandler<VehicleContainer>() {
private val lobbyService: LobbyService by lazy {
NetModule.provideRetrofit().create(LobbyService::class.java)
}
override fun makeRequest(): Call<VehicleContainer> {
//Retrofit interface method
return lobbyService.getManufacturers()
}
}
- Note put in your retrofit request call into the
makeRequest()method.
After creating your request handler as above,
To make the actual api call, create an object of the request class and call the doRequest().
fun getManufacturer() {
LobbyRequest(LobbyRequest.MANUFACTURER).doRequest().observe(this, object : Kobserver<VehicleContainer>() {
override fun onException(exception: Exception) {
//handle exceptions here, custom exception types inclusive
}
override fun onSuccess(data: VehicleContainer) {
}
})
}
viewModel._liveData.observe(this, object : Kobserver<ResponseModel>() {
override fun onSuccess(data: ResponseModel) {
Toast.makeText(this@MainActivity, "Works", Toast.LENGTH_LONG).show()
}
override fun onException(exception: Exception) {
userErrorHanlder(exception)
}
})
private fun userErrorHanlder(ex: Exception) {
when (ex) {
is GitHubErrorHandler.ErrorConfig.NetworkException -> {
Toast.makeText(this@MainActivity, ex.message, Toast.LENGTH_LONG).show()
}
is GitHubErrorHandler.ErrorConfig.GitHubException -> {
Toast.makeText(this@MainActivity, ex.message, Toast.LENGTH_LONG).show()
}
else -> Toast.makeText(this@MainActivity, "Oops! Something went wrong.", Toast.LENGTH_LONG).show()
}
}
As noted above the Request class doRequest() executes the api call and depending on usage it could either
return an observable live data object or a data wrapper of type Wrapper<T> were T represents your object, within this wrapper class
you have access to your data or exceptions as well as Status code.
Now Ketro offers an extension of the Android Observer class(Kobserver<T>), which attempts to handle api errors/exceptions delegated by the user,
hence why we have an exception and a success callback.
- Note Using the Kobserver with the returned api response is optional, but recommended for proper error handling.
There are situations where you may want to have a separate request method and a separate LiveData object update when the request resolves. In such scenarios,
instead of calling doRequest(), we would call executeRequest(liveData: MutableLiveData<Wrapper<R>>) or use the Coroutines helper
as described in the next section.
This method needs the specified response type to be wrapped with the Wrapper class in Ketro so it can propagate errors effectively.
Internally all the methods wrap each object with the Ketro Wrapper.
val wrap = MutableLiveData<Wrapper<VehicleContainer>>()
fun getManufacturer() {
LobbyRequest(LobbyRequest.MANUFACTURER).executeRequest(responseLiveData)
}
After the request is resolved, the LiveData object passed in will have its value set with the response and all active observers of the LiveData are triggered.
2. Request API Implementation with Coroutines helper
This sections shows examples on how to use the Request API, the samples provided will come in pairs, one would be for
responses using the execute -> KResponse<T> and the other would be for doRequest -> Wrapper<T>.
Note: The request api uses Coroutine Suspend functions.
Create your class holding your network requests or you can use one class per request, the doRequest():Wrapper<T> suspention method from
the Request class is used to make the network call:
class CoRoutineSampleRequest {
private val gitHubAPI: GitHubAPI by lazy {
NetworkModule.createRetrofit().create(GitHubAPI::class.java)
}
suspend fun requestGithubUser(user: String): Wrapper<ResponseItems> {
val req = object : Request<ResponseItems>(GitHubErrorHandler()) {
override suspend fun apiRequest(): Response<ResponseItems> =
gitHubAPI.searchUse(user)
}
return req.doRequest()
}
}
Example using Request API
class GetUserDataSourceRemote @Inject constructor(private val gitHubAPI: GitHubAPI) {
suspend fun requestGithubUser(user: String): KResponse<ResponseItems> {
val req = object : Request<ResponseItems>(GitHubErrorHandler()) {
override suspend fun apiRequest(): Response<ResponseItems> =
gitHubAPI.searchUse(user)
}
return req.execute()
}
}
Override the errorHandler class for adding extra/custom ErrorHandling exceptions details in next section Error Handling.
Note:
The doRequest method returns a Wrapper with the response encapsulated within it and returns the error/error code and custom exceptions
as you may have defined.
private val viewModelJob = SupervisorJob()
//Scope coroutine to a ViewModel or use global scope
private val scope = CoroutineScope(Dispatchers.Default + viewModelJob)
fun getGitHubUser() {
scope.launch {
val user = getUserUseCase(name)
withContext(Dispatchers.Main) {
liveData.value = user
}
}
}
private val _errorLiveData: MutableLiveData<Exception> = MutableLiveData()
val errorLiveData: LiveData<Exception> = _errorLiveData
private val liveDataHandler = LiveDataHandler(_errorLiveData)
private val liveData = MutableLiveData<Items>()
val _liveData: LiveData<Items> = liveData
private val viewModelJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default
+ viewModelJob)
fun searchUser(name: String) {
scope.launch(handler()) {
val user = getUserUseCase(name)
withContext(Dispatchers.Main) {
liveDataHandler.emit(user, liveData)
}
}
}
Above is an example of handling data response of type KResponse in the ViewModel and emitting either a failure or the success T
The LiveDataHandler is a function within ketro that parses the KResponse return object and can emit either the success or failure object.
private val _errorLiveData: MutableLiveData<Exception> = MutableLiveData()
val errorLiveData: LiveData<Exception> = _errorLiveData
private val liveDataHandler = LiveDataHandler(_errorLiveData)
To use the LiveDataHandler initialise it with a LiveData that takes in an exception i.e LiveData<Exception> this
is because the KResponse wraps errors in exceptions that can be predefined by the user Using the ApiErrorHandler which allows you to OverRide and add your own error
definitions. Then on the view you can collect your LiveData values as normal because if success the LiveDataHandler will
emit the success value to the specific LiveData.
viewModel._liveData.observe(this, Observer {
toggleViews(true)
Toast.makeText(this@MainActivity, "Works", Toast.LENGTH_LONG).show()
})
viewModel.errorLiveData.observe(this, Observer { ex ->
ex?.let {
userErrorHanlder(it)
}
})
Error Handling
Handling custom errors with Ketro is quite simple, the library expects you use either the response code gotten from your server or a custom message gotten from your server and map an Exception to which would be return to the request class by overriding the error handler object to return your class with your error mapping implementation.
Note if this is not provided, a default exception is returned and propaged to the views callback interface.
First off you need to create a class which extends ApiErrorHandler then you can either put your own Exception cases there or create a new class for each exception case depends o
