Blog Infos
Author
Published
Topics
Published
Topics

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.

Proxy

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(
        MyInterface::class.java.classLoader,
        arrayOf(MyInterface::class.java)
    ) { 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
        null
    } 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.

Implementing Retrofit

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:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GET(val baseUrl: String)

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
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()) }
    }.build()
    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)
                              .execute().body().string()
            return objectMapper.readValue(response, responseClass)
        }
    }
}

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

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

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 {
    @GET("http://samples.openweathermap.org/data/2.5/weather")
    fun getWeather(@Query("q") city: String, @Query("appid") apiKey: String): Call<Weather>

    @GET("http://samples.openweathermap.org/data/2.5/uvi")
    fun getUvIndex(@Query("lat") lat: Double, @Query("lon") lon: Double, @Query("appid") apiKey: String): Call<UvIndex>
}

fun main(args: Array<String>) {
    val API_KEY = "YOUK KEY"
    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()
    println(weather)
    println(uvIndex)
}
The full implementation

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

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Proxy
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GET(val baseUrl: String)
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Query(val parameterName: String)
interface Call<out T> {
fun execute(): T
}
class SimpleRetrofit {
val objectMapper: ObjectMapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val httpClient = OkHttpClient()
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()) }
}.build()
return Request.Builder().url(url).build()
}
private fun <T> createCall(request: Request, responseClass: Class<T>): Call<T> {
return object : Call<T> {
override fun execute(): T {
val response = httpClient.newCall(request).execute().body().string()
return objectMapper.readValue(response, responseClass)
}
}
}
private fun extractResponseType(method: Method) = (method.genericReturnType as ParameterizedType).actualTypeArguments[0] as Class<*>
fun <T> createService(serviceClass: Class<T>): T = 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
}
data class Weather(val main: Main)
data class Main(val temp: Double)
data class UvIndex(val value: Double)
interface OpenWeatherMapApi {
@GET("http://samples.openweathermap.org/data/2.5/weather")
fun getWeather(@Query("q") city: String, @Query("appid") apiKey: String): Call<Weather>
@GET("http://samples.openweathermap.org/data/2.5/uvi")
fun getUvIndex(@Query("lat") lat: Double, @Query("lon") lon: Double, @Query("appid") apiKey: String): Call<UvIndex>
}
fun main(args: Array<String>) {
val API_KEY = "YOUK KEY"
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()
println(weather)
println(uvIndex)
}

This time was Retrofit. The last time was ButterKnife by

. Next time we will overcome the restriction imposed by Proxy, and create a simple version of a Mock Library like Mockito or Mockk. For now, you can see all the articles here.

References

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu