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.jsonToObject
tofun <T> String.jsonToObject
and see if it works. – Leo Asoinline
andreified
keywords 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. – Quaffel