Let Kotlin do the code for you — Part II: Retrofit and Proxy | by André Oriani | Aug, 2023
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(
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 MyInterfacedynamicObject.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:
@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 :
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:
Related Posts
Leave a Reply Cancel reply
Categories
- ! Без рубрики (1)
- ++PU (1)
- 1 (1)
- 1w (1)
- 1win Brazil (1)
- 1win India (1)
- 1WIN Official In Russia (1)
- 1win Turkiye (1)
- 1xbet egypt (1)
- 2ankarafayansustasi.net_may (1)
- ankarafayansustasi.netsiteai apr (1)
- Artificial intelligence (1)
- Arts & Entertainment, Photography (1)
- belugasitesi_mAY (1)
- BH_TOPsitesi apr (1)
- BHsitesy_may (2)
- Blog (3)
- Bookkeeping (14)
- Bootcamp de programação (2)
- Bootcamp de programación (2)
- BT_TOPsitesi apr (1)
- casino (5)
- casinom-hub (1)
- casinom-hub.comsitesi apr (3)
- colombian mail order brides (1)
- Cryptocurrency exchange (2)
- Dinamobet_next (1)
- Disease & Illness, Colon Cancer (1)
- Dumanbet (1)
- Dumanbet_next (1)
- Finance, Insurance (1)
- FinTech (5)
- Forex Trading (11)
- Galabet (1)
- Health & Fitness, Fitness Equipment (1)
- Hitbet (1)
- Home & Family, Crafts (1)
- Home & Family, Gardening (1)
- Internet Business, Audio-Video Streaming (1)
- Internet Business, Ecommerce (1)
- Internet Business, Email Marketing (1)
- Internet Business, Internet Marketing (1)
- IT Вакансії (1)
- IT Образование (5)
- IT Освіта (1)
- latin women dating (1)
- mail order bride (1)
- Mars bahis (2)
- Matadorbet (1)
- minimiri.comsitesi apr (3)
- Mobile App Development (771)
- Mostbet Russia (1)
- New Post (1)
- News (12)
- PB_TOPsitesi apr (1)
- PBsitesi_may (1)
- Pusulabet (1)
- redmirepool.bizsitesi apr (2)
- redmirepoolsitesi_may (1)
- Reference & Education, College (1)
- Reference & Education, Sociology (1)
- Rokusitesi apr (1)
- Sober living (6)
- Society, Divorce (1)
- Software development (7)
- Superbetin (1)
- Tempobet_next (1)
- thelongeststride.comsitesi apr (1)
- tipobet-turkiyesitesi apr (1)
- Ultrabet (1)
- Uncategorized (1)
- Игра (2)
- казино (1)
- Криптовалюты (1)
- Новости Криптовалют (1)
- Финтех (7)
- Форекс Брокеры (9)
- Форекс обучение (2)