Learning how to create a simple version of Retrofit from scratch

A line of robots typing code in computers, pencil art — DALLE-2

Retrofit is undoubtedly one of the most important libraries for Android development. It allows implementing REST APIs by just specifying an interface, without having to deal with all the details of OkHttp. It is pretty flexible: you can choose any sort of marshaling library, and you can pick among callbacks, RxJava, or even coroutines. But have you wondered how all that magic happens? The solution to that puzzle is Proxy. Not a network proxy or a proxy like the design pattern defined by GoF. I am talking about the Java class Proxy from the reflect package.

This article is part of a series. To see the other articles click here.

The Proxy class allows you to dynamically implement interfaces by intercepting calls to methods. All you need to do is to implement an InvocationHandler, some sort of listener that tells you which method was called and its parameters.

Let’s start with an elementary interface:

data class Person(val name: String, val surname: String)
interface MyInterface {
fun methodOne(param1: String, param2: Int)
fun methodTwo(param: Person): String

To implement MyInterface dynamically, we just need a few lines of code:

fun main() {
val dynamicObject = Proxy.newProxyInstance(
) { proxy, method, args ->
println("Called ${method.toGenericString()} with params: ${Arrays.toString(args)}")
// Returning null so we don't have to deal with the return type for now
} as MyInterface

dynamicObject.methodOne("Hello", 42)
dynamicObject.methodTwo(Person("Julius", "Caesar"))

And those lines of code will print:

Called public abstract void MyInterface.methodOne(java.lang.String,int) with params: [Hello, 42]
Called public abstract java.lang.String MyInterface.methodTwo(Person) with params: [Person(name=Julius, surname=Caesar)]

Important: Proxy can only be used with interfaces. If you use it with a class it will crash:

Exception in thread "main" java.lang.IllegalArgumentException: MyClass is not an interface
at java.base/java.lang.reflect.Proxy$ProxyBuilder.validateProxyInterfaces(Proxy.java:706)
at java.base/java.lang.reflect.Proxy$ProxyBuilder.<init>(Proxy.java:648)
at java.base/java.lang.reflect.Proxy$ProxyBuilder.<init>(Proxy.java:656)
at java.base/java.lang.reflect.Proxy.lambda$getProxyConstructor$0(Proxy.java:429)
at java.base/jdk.internal.loader.AbstractClassLoaderValue$Memoizer.get(AbstractClassLoaderValue.java:329)
at java.base/jdk.internal.loader.AbstractClassLoaderValue.computeIfAbsent(AbstractClassLoaderValue.java:205)
at java.base/java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:427)
at java.base/java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1037)
at MainKt.main(Main.kt:30)
at MainKt.main(Main.kt)

We will learn how to overcome that limitation in the next chapter when I will tell you how mock libraries like Mockito and Mockk work.

Now that we know the basics, we shall be able to implement a simplified version of Retrofit. Our version will only support HTTP GET requests and query parameters. Thus, our first step is to define a couple of annotations to define the request’s URL and query parameters:

annotation class GET(val baseUrl: String)

annotation class Query(val parameterName: String)

To keep this implementation short, our version of Call will only allow synchronous requests:

interface Call<out T> {
fun execute(): T

We now need to extract the values of the annotations of the methods of the interface in order to create an OkHttp Request:

private fun createRequest(method: Method, args: Array<Any?>): Request {
val baseUrl = method.getAnnotation(GET::class.java).baseUrl
val paramNames = method.parameterAnnotations.flatten().map { (it as Query).parameterName }
val url = HttpUrl.parse(baseUrl).newBuilder().apply {
paramNames.forEachIndexed { index, paramName -> addQueryParameter(paramName, args[index].toString()) }
return Request.Builder().url(url).build()

We also need to extract the actual response type from the method thru reflection:

private fun extractResponseType(method: Method): Class<*> {
return (method.genericReturnType as ParameterizedType).actualTypeArguments[0] as Class<*>

Now that we created the request and we know the response type, we can create the network call and parse the response:

private fun <T> createCall(request: Request, responseClass: Class<T>): Call<T> {
return object : Call<T> {
override fun execute(): T {
val response = httpClient.newCall(request)
return objectMapper.readValue(response, responseClass)

Now that we have all the basic blocks, we can dynamically implement the interface :

fun <T> createService(serviceClass: Class<T>): T {
return Proxy.newProxyInstance(serviceClass.classLoader, arrayOf(serviceClass)) {
thiz: Any, method: Method, args: Array<Any?> ->
val request = createRequest(method, args)
val responseType = extractResponseType(method)
createCall(request, responseType)
} as T

With our simplified implementation of Retrofit done, we can now try it with the OpenWeather API :

data class Weather(val main: Main)
data class Main(val temp: Double)
data class UvIndex(val value: Double)

interface OpenWeatherMapApi {
fun getWeather(@Query("q") city: String, @Query("appid") apiKey: String): Call<Weather>

fun getUvIndex(@Query("lat") lat: Double, @Query("lon") lon: Double, @Query("appid") apiKey: String): Call<UvIndex>

fun main(args: Array<String>) {
val service = SimpleRetrofit().createService(OpenWeatherMapApi::class.java)
val weather = service.getWeather("London", API_KEY).execute()
val uvIndex = service.getUvIndex(37.75, -122.37, API_KEY).execute()

The full implementation

And here is how all the pieces we built above were put together:

Source link