AndroidPub
Published in

AndroidPub

Building your own Retrofit Call Adapter

Photo by James McKinven on Unsplash

The use of RxJava has really shielded us away from topics like this because RxJava is now a ubiquitous term with android development, so there is almost no reason to adapt your retrofit responses since we rely on Rx observables for most apps.

I recently had to write a library that required retrofit for network requests, and per the request of the team lead, we were not to include Rx. This was kind of a shocker because I realized I would have to go back to passing interface callbacks to my asynchronous requests, for example:

httpService.getUsers(12).enqueue(object : Callback<R> {

override fun onFailure(call: Call<R>?, t: Throwable?) {
// handle error
}

override fun onResponse(call: Call<R>?, r: Response<R>?) {
// handle success
}

})

I wasn’t going to do this, so I decided to go on the journey of writing my own simple call adapter that will just provide the response and throwable for a single callback function, just like with Rx subscribe method.

Here are the things you need to do to achieve that.

Transforming the retrofit Call object

The original way of providing responses, without any adapters, is using the retrofit Call object

interface IHttpService {

@GET("users")
fun getUsers(@Query("results") result: Int): Call<List<Users>>

}

and what we are trying to achieve is, transform this call object to provide our own wrapper, for the response

interface IHttpService {

@GET("users")
fun getUsers(@Query("results") result: Int): Simple<List<Users>>

}

notice that the generic wrapper is no longer call but simple. It is this simple object that will be responsible for issuing out the request and providing a compound result of error and response so that you can use it like this:

httpService.getUser(12).process { users, throwable ->
//
handle error or response
}

That process function that you see above, is a member of the Simple class and will be responsible for issuing out the request and invoking your callback.

Defining the Simple Transformer

We need to define the simple class so that it performs both asynchronous and synchronous network calls using the call object. That would look like this:

class Simple<R>(private val call: Call<R>) {
fun run(responseHandler: (R?, Throwable?) -> Unit) {
// run in the same thread
try {
// call and handle response
val response = call.execute()
handleResponse(response, responseHandler)

} catch (t: IOException) {
responseHandler(null, t)
}
}

fun process(responseHandler: (R?, Throwable?) -> Unit) {
// define callback
val callback = object : Callback<R> {

override fun onFailure(call: Call<R>?, t: Throwable?) =
responseHandler(null, t)

override fun onResponse(call: Call<R>?, r: Response<R>?) =
handleResponse(r, responseHandler)
}

// enqueue network call
call.enqueue(callback)
}
private fun handleResponse(response: Response<R>?, handler: (R?, Throwable?) -> Unit) { if (response?.isSuccessful == true) {
handler(response.body(), null)
} else {
if (response?.code() in 400..511)
handler(null, HttpException(response))

else handler(response?.body(), null)

}
}
}

Now let’s talk about this class, and there are a few points to note:

  • First, it takes a call object in its constructor, which is the object containing the HTTP request to be issued out.
  • The run method is used to synchronously issue out network requests, and pass results to the response handler
  • The process method is used to asynchronously issue out network requests, and pass the results to the response handler

so with this class set as your return type for your network request, you can do this:

// run synchronously
httpService.getUser(12).run { users, throwable ->
//
handle error or response
}
// run asynchronously
httpService.getUser(12).process { users, throwable ->
//
handle error or response
}

Creating the Call Adapter

To let retrofit know of this transformer we defined above, we have to create an implementation of the retrofit CallAdapter interface like so:

class SimpleCallAdapter<R>(private val responseType: Type): CallAdapter<R, Any> {

override fun responseType(): Type = responseType

override fun adapt(call: Call<R>): Any = Simple(call)
}

From the code above you see that the Call Adapter is responsible for two things:

  • Providing a Type object that will be the type of the request’s response, that is, the type of the object in the HTTP response body. (i.e. ‘List<User>’ in Simple<List<User>>)
  • providing the transformer that will transform the retrofit call object: which is what we had defined earlier (Simple<R>).

Defining the Call Adapter Factory

The last thing we need to do is let retrofit know of this call-adapter and to that, we use a CallAdapter.Factory implementation. This will be defined as like:

class SimpleCallAdapterFactory private constructor() : CallAdapter.Factory() {

override fun get(returnType: Type?, annotations: Array<out Annotation>?, retrofit: Retrofit?): CallAdapter<*, *>? {
return returnType?.let {
return try {
// get enclosing type
val enclosingType = (it as ParameterizedType)

// ensure enclosing type is 'Simple'
if (enclosingType.rawType != Simple::class.java)
null
else {
val type = enclosingType.actualTypeArguments[0]
SimpleCallAdapter<Any>(type)
}
} catch (ex: ClassCastException) {
null
}
}
}

companion object {
@JvmStatic
fun create() = SimpleCallAdapterFactory()
}

}

We see that this Factory is meant to implement only one method, get. This get method takes a couple of parameters, which I will explain shortly, and it must return an implementation of CallAdapter.

You can ignore the nullable types in the parameter list, that is just a result of Kotlin and Java interop. But here is what each parameter is:

  • return type: This is the return type of the calling function, for instance, the method getUsers of IHttpService, is Simple<List<User>>
  • annotations: This is a list of annotations that the calling method has, for instance, the method getUsers of IHttpService, will have one annotation, which is the retrofit GET annotations.
  • retrofit: This is the retrofit instance that created the http-service containing the calling method.

Now let us talk about what’s going on inside the function. Since the function is meant to return an implementation of CallAdapter, you guessed it, we return our defined SimpleCallAdapter. But before we do that we need to use the parameter to determine whether we return our CallAdapter or null.

That right, null. This is done like this because the retrofit instance can contain multiple call adapter-factories for different call adapters, so we return null when the return type is not our concern (i.e. it is not a ‘Simple’ type)

This is precisely what the function is doing. It checks that the parameterized type is a Simple type, and then creates our simple adapter with the type parameter argument as the response-type, else it returns null.

The last thing of note is our static create function in the companion object, which is responsible for creating an instance of the adapter factory.

Finally adding the Factory to Retrofit

This part is the simplest, all you need to do is call the static create function of the Factory and add that instance to retrofit, using the retrofit builder like so:

Retrofit retrofit = new Retrofit.Builder()
//... other configs
.addCallAdapterFactory(SimpleCallAdapterFactory.create())
.build();

and that is it, now retrofit will use this Adapter Factory to create an Adapter and use the created Adapter to transform the Http Call object.

Other Notes

There are two other concerns that you need to be aware of :

  • The first is support for Java using a functional interface so that Java devs don’t have to write ugly Kotlin Function classes.
  • The second concern is, the beauty for RxJava adapters is the ability to cancel subscriptions when the response is no longer needed. The current implementation doesn’t have this.

These two concerns can be addressed ass follows:

First, create an interface for Java devs and add that to overloaded methods like this:

// create functional interface
public interface SimpleHandler<T> {
void accept(T response, Throwable throwable);
}

Add overloaded methods to the Simple transformer, that will require this interface as a callback:

class Simple<R>(private val call: Call<R>) {

// support for java
fun run(responseHandler: SimpleHandler<R?>) =
run(responseHandler::accept)
fun process(responseHandler: SimpleHandler<R?>) =
process(responseHandler::accept)
//... other members

}

The second problem is providing a way to cancel subscriptions, to prevent a callback method from being called. This is applicable to only asynchronous functions, thus the only part we need to be concerned about is the process function:

First let us create a Subscription class that can hold a shared state:

class Subscription {

private var disposed = false

fun isDisposed() = disposed

fun dispose() {
disposed = true
}
}

We will now use this class to share the state of subscription between the process function and the enclosing class. We do this by creating a subscription and returning it from the process function:

class Simple<R>(private val call: Call<R>) {

// ... other memebers
fun process(responseHandler: (R?, Throwable?) -> Unit): Subscription {

val subscription = Subscription()

// define callback
val callback = object : Callback<R> {

override fun onFailure(call: Call<R>?, t: Throwable?) {
if (!subscription.isDisposed())
responseHandler(null, t)
}

override fun onResponse(call: Call<R>?, response: Response<R>?) {
if (!subscription.isDisposed())
handleResponse(response, responseHandler)
}

}

// enqueue network call
call.enqueue(callback)
// return subscription
return subscription
}
// ... other memebers}

Then in your calling class, you can do this:

val subscription = httpService.getUsers(12).process { r, t ->
//
handle success or response
}


// dispose subscription
subscription.dispose()

Conclusion

In this post, I have shown you how to:

  • Create your own Call transformer
  • Create your own CallAdapter
  • Create your own CallAdapter.Factory
  • Make retrofit aware of your Call Adapter

That is it, you can check out my github repo.

--

--

--

The (retired) Pub(lication) for Android & Tech, focused on Development

Recommended from Medium

12 Essential Object Manipulation Methods for JavaScript Devs in 2022

Create a Portfolio Website and host on Git-Hub

Responsive images sizes improve application performance

How to scroll to first invalid control once a form has been submitted

Data-Independent Automated Tests

8 JavaScript Best Practices for Beginners

How To Test Your React Container Components

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ebi Igweze

Ebi Igweze

A software developer and a mathematical enthusiast. For mentorship find me on code mentor https://www.codementor.io/@ebiigweze

More from Medium

How I learned to automate Android apps with Android Espresso

cloud based Espresso testing

Android Security & User Privacy — Part 2

Migrate to Jetpack Compose from XML in Android

Chat implementation in Android with Firebase Cloud Firestore database