I am creating http json client. I am using Volley in combination with coroutines. I wanted to create generic http client so I can use it everywhere.
I have created generic extension method to parse JSON string into object.
inline fun <reified T>String.jsonToObject(exclusionStrategy: ExclusionStrategy? = null) : T { val builder = GsonBuilder() if(exclusionStrategy != null){ builder.setExclusionStrategies(exclusionStrategy) } return builder.create().fromJson(this, object: TypeToken<T>() {}.type) }
Problem is that when I call this method I don't get expected result. First call gives proper result. Object is initialized. But second call, where I use generic parameter which is passed to method, ends with exception "LinkedTreeMap can not be cast into Token".
protected inline fun <reified T>sendRequestAsync(endpoint: String, data: Any?, method: Int, token: Token?): Deferred<T> { return ioScope.async { suspendCoroutine<T> { continuation -> val jsonObjectRequest = HttpClient.createJsonObjectRequest( endpoint, data?.toJsonString(), method, Response.Listener { //this call is successful and object is initialized val parsedObject : HttpResponse<Token> = it.toString().jsonToObject() //this call is not successful and object is not initialized properly val brokenObject : HttpResponse<T> = it.toString().jsonToObject() continuation.resume(brokenObject.response) }, Response.ErrorListener { continuation.resumeWithException(parseException(it)) }, token) HttpClient.getInstance(context).addToRequestQueue(jsonObjectRequest) } } } Call of generic method.
fun loginAsync(loginData: LoginData): Deferred<Token> { return sendRequestAsync("/tokens/", loginData, Request.Method.POST, null) } This is how httpresponse data class looks.
data class HttpResponse<T> ( val response: T ) I saw a workaround here using Type::class.java but I don't like this approach and I would like to use reified and inline keywords. How does the reified keyword in Kotlin work?
UPDATE This is exception which I am getting.
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xbionicsphere.x_card.entities.Token
POSSIBLE WORKAROUND I found possible workaround. If I create method which will parse Token into from response and use this method in executeRequestAsync everything starts working but I don't like this solution since I have to add additional parameter for each request.
New loginAsync
fun loginAsync(loginData: LoginData): Deferred<Token> { val convertToResponse : (JSONObject) -> HttpResponse<Token> = { it.toString().jsonToObject() } return executeRequestAsync("/tokens/", loginData, Request.Method.POST, null, convertToResponse) } New executeRequestAsync
protected inline fun <reified T>executeRequestAsync(endpoint: String, data: Any?, method: Int, token: Token?, crossinline responseProvider: (JSONObject) -> HttpResponse<T>): Deferred<T> { return ioScope.async { suspendCoroutine<T> { continuation -> val jsonObjectRequest = HttpClient.createJsonObjectRequest( endpoint, data?.toJsonString(), method, Response.Listener { val response: HttpResponse<T> = responseProvider(it) continuation.resume(response.response) }, Response.ErrorListener { continuation.resumeWithException(parseException(it)) }, token ) HttpClient.getInstance( context ).addToRequestQueue(jsonObjectRequest) } } } UPDATE I probably have found working solution. executeRequestAsync needs final type definition provided through generic parameters so I enhanced declaration of method. Now method declaration looks like this:
protected inline fun <reified HttpResponseOfType, Type>executeRequestAsync(endpoint: String, data: Any?, method: Int, token: Token?) : Deferred<Type> where HttpResponseOfType : HttpResponse<Type> { val scopedContext = context return ioScope.async { suspendCoroutine<Type> { continuation -> val jsonObjectRequest = HttpClient.createJsonObjectRequest( endpoint, data?.toJsonString(), method, Response.Listener { val response: HttpResponseOfType = it.toString().jsonToObject() continuation.resume(response.response) }, Response.ErrorListener { continuation.resumeWithException(parseException(it)) }, token ) HttpClient.getInstance( scopedContext ).addToRequestQueue(jsonObjectRequest) } } } Thanks this complicated function declaration I can execute request with this call:
fun loginAsync(loginData: LoginData): Deferred<Token> { return executeRequestAsync("/tokens/", loginData, Request.Method.POST, null) }
inline fun <reified T> String.jsonToObjecttofun <T> String.jsonToObjectand see if it works.inlineandreifiedkeywords makes perfect sense as type erasure comes into effect. Instead of representing the type you pass to the type variable T, T would represent Object at runtime. It's therefore impossible for Gson to determine which type you want to deserialize. I expect a similar effect during your second call, but I'm not sure yet.