Giter Club home page Giter Club logo

smilecs / ketro Goto Github PK

View Code? Open in Web Editor NEW
89.0 5.0 11.0 541 KB

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/

License: MIT License

Kotlin 100.00%
kotlin kotlin-library android-library livedata coroutines-android errorhandler retrofit-calls request-handler propagate-errors wrappers

ketro's Introduction

ketro

Alt text

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:

  1. GenericRequestHandler<T>
  2. 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 on your preference. Ketro now provides a Kexception class which return the Respoonse Error Body, though the getExceptionType returns the exception super type, so as to make using the Kexception class optional.

import com.past3.ketro.api.ApiErrorHandler
import retrofit2.Response

class LobbyErrorHandler : ApiErrorHandler() {

    override fun getExceptionType(response: Response<*>): Exception {
        return when (response.code()) {
            LOGIN_ERROR -> LoginException(response.errorBody(), response.message())
            UPDATE_ERROR -> UpdateException()
            else -> Exception()
        }
    }

    companion object ErrorConfig {
        const val LOGIN_ERROR = 401
        const val UPDATE_ERROR = 404
        //sub-classing kexception allows you to have access to the errorbody
        class LoginException(val errorBody: ResponseBody?, message: String?, cause: Throwable? = null)
         : Kexception(message, cause) {
            override val message = "Error processing login details"
        }

        class UpdateException : Exception() {
            override val message = "Error updating details"
        }
    }

}

Now you can choose to map your errors anyway you like to an exception, for me I prefer to use http error status codes to determine what kind of exception I return to the Wrapper object you can as well choose to return an error object from your server and map that out to your exception, the possibilities are endless.

Also, remember the request class you created earlier? you will need to override the ApiErrorhandler field and initialise your custom class, the rest will be handled by Ketro.

class LobbyRequest(private val page: Int) : GenericRequestHandler<VehicleContainer>() {

    private val lobbyService: LobbyService by lazy {
        NetModule.provideRetrofit().create(LobbyService::class.java)
    }

    override val errorHandler: ApiErrorHandler = LobbyErrorHandler()

    override fun makeRequest(): Call<VehicleContainer> {
        return lobbyService.getManufacturers(page, pageSize, Urls.KEY)
    }
}

After creating your class and modifying your request handler you can go ahead to check for the exception in your View(Activity/Fragment) Here you can Also check if the exception is of type Kexception and use the errorBody included within the object. Note: the onException override is now optional, and as well you can pass in a function into the kobserver constructor to handle your errors and as well get a cleaner interface

viewModel.responseData().observe(this, object : Kobserver<List<VehicleContainer>>() {
            override fun onException(exception: Exception) {
             when (exception) {
                is Kexception -> {
                    //exception.errorBody do something with errorBody
                 }
                 is LobbyErrorHandler.ErrorConfig.UpdateException ->{
                    // handle error e.g. show dialog, redirect user etc.
                 }
             }
            }

            override fun onSuccess(data: List<VehicleContainer>) {
                // Update UI
                swipeRefresh.isRefreshing = false
                searchView.visibility = View.VISIBLE
                helperContainer.visibility = View.VISIBLE

                viewModel.genericList.addAll(data)
                vehicleAdapter.notifyDataSetChanged()
                searchView.setAdapter(searchAdapter)
                searchAdapter.notifyDataSetChanged()

            }
        })

Alternatively

Passing in an error handling function will allow you to omit the onException callback which is now optional when using the Kobserver.

Note: that passing in a function will stop the onException callback from executing

so it's a choice between using either one.

private fun userErrorHanlder(ex: Exception) {
        //handle errors here
}

 viewModel.searchUser(editText.text.toString()).observe(this, object : Kobserver<ResponseModel>(::userErrorHanlder) {
            override fun onSuccess(data: ResponseModel) {
                if (data.items.isEmpty()) {
                    toggleViews(false)
                    return
                }
                toggleViews(true)
                viewModel.list.let {
                    it.clear()
                    it.addAll(data.items)
                    listAdapter.submitList(it)
                }
            }

        })

Also for any request or anything unclear with the library feel free to hit me up, on [email protected] or create an issue ticket.

ketro's People

Contributors

chizoba avatar smilecs avatar zsmb13 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

ketro's Issues

Deprecate LiveData handlers

Is your feature request related to a problem? Please describe.
N/A
Describe the solution you'd like
Deprecate LiveData Handlers by adding deprecation annotations to those methods and add sample implementation using suspend function calls

Describe alternatives you've considered
Universal flow emitter?

Additional context
N/A

Add Coroutines support

Is your feature request related to a problem? Please describe.
To help developers easily use this library with the current approved modularised architecture and the use of coroutines, it would be nice for ketro to support coroutines and offer a way to easily aggregate responses within the data module of an app and return to the presentation layer

Describe the solution you'd like
Add or modify existing methods to be of type suspend so as to make them accessible or usable within a coroutine context or scope

Describe alternatives you've considered
Thought of using the executerequest method and wrapping it with a suspend function, but it seems a bit too hacky

Additional context
N/A

Migrate to android x,update sdk version and dependencies

Is your feature request related to a problem? Please describe.
N/A
Describe the solution you'd like
Update all outdated dependencies and also migrate to android x

Describe alternatives you've considered
N/A

Additional context
N/A

Exposed onFinished method in Kobserver

There is a method which is called onFinished within the Kobserver that is always called when a network request is completed and data can be sent to the view. This method is called irrespective of an error or success, making it the best place to hide loading animations or dialogs.
The current Kobserver has this method but it's not open(hidden) for a subclass to override it.

Error Body along with Exception

Description
When the API call fails for one of the 40XX client errors, the onException method gets called where the ApiErrorHandler() class returns an Exception with a message property but not the error body. The Exception class has a property message that can be overriden in the custom error handler class and a message can be sent back to the observer as part of the exception. As the Exception class doesn't have a ResponseBody property, a responseBody cannot be sent with the exception. The responseBody can be converted to a string using the toStirng() method and sent as a message but it makes it very hard for the observer to convert it back to a ResponseBody. It's nice to have the library take care of this as the boiler plate is nicely setup in the library already.

Use case
APIs return response bodies of a specific model to the client so that the client can convert the responseBody to that model object which can then be used to control views.

Expected Result
The onSuccess method should get called for success response that doesn't have a body.

Notes for dev
Modify the ApiErrorHandler to return something with exception and also includes a property ResponseBody. Using this, the custom error handler class can send response.errorBody as part of the exception that the observer can use as below:

override fun onException(exception: Exception) {
                val errorBody = exception.errorBody
    // converter is a function to convert errorBody to errorDetails
               val errorDetails = converter.convert(errorBody)
               textView.text = errorDetails.errors[0].message
            }

ErrorDetails Model:

class ErrorDetails : Serializable {
   var errors: List<Error>? = null

    inner class Error : Serializable {
        var message: String? = null
        var price: Double = 0.0
        var tax: Double = 0.0
    }
}

Example errorBody returned by the api
Body: {"errors":[{"message":"Balance too low","price":10,"tax":3}]}

Void Return support

Description
There is no support for Void return from an API call. For example, a 201 response that doesn't have a body. I looked at the source code and found that success is determined based on whether there is a body or not.

Expected Result
The onSuccess method should get called for success response that doesn't have a body.

Notes for dev
response.isSuccessful should be checked after the conditional statement response.body() != null

Parse method should take in nullable function

Is your feature request related to a problem? Please describe.
Currently, the parse method in the LiveDataHandler method takes in a non-nullable function parameter, this should be edited to take in a nullable function.

Describe the solution you'd like
The parse method should be able to take in a non-nullable function or it should have a default empty function.

Describe alternatives you've considered
N/A

Additional context
N/A

Add generic executeRequest method

Currently when making a wrapped request, you call the executeRequest method which takes in a LiveData object with signature of Wrapper<T>. Add a new executeRequest method which can take in a LiveData object of a plain generic type LiveData<T> instead of LiveData<Wrapper<T>>

App is crashed when no internet connection while api calling

Describe the bug
App is crashed when you disable your internet connection and call api

To Reproduce
Steps to reproduce the behavior:

  1. Install sample app from ketro github repository
  2. Disable internet connection in device
  3. Enter any GitHub user name and click on search button to call api
  4. App is crashed

Expected behavior
It should show message to user for no connection

Smartphone:

  • Device: Le X526
  • OS: Android 6.0.1

Additional context
How can we handle this scenario by using ketro?

Improve Wrapper object properties

Is your feature request related to a problem? Please describe.
The current wrapper object has a data, exception and status code property and I feel these should be simplified to fit the purpose of the original intent of the wrapper object which is to return a response in a concise way to denote failure or success

Describe the solution you'd like
The data and exception properties should be removed and replaced with a Sealed class which would have two children a success and failure child, that each has its respective properties of either data or exception.

Describe alternatives you've considered

Additional context
This would allow the ViewModel or any other consumer of the Wrapper object to take check against the sealed class type and write logic against this match.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.