Home > Software design >  How to write generic functions in Kotlin interfaces's implementations
How to write generic functions in Kotlin interfaces's implementations

Time:03-09

I am trying to implement a generic HttpClient like this one:

interface HttpClient {
    fun <T: Any> get(url: String): T?
}

implemented by a class like this:

class HttpClientImpl @Inject constructor(...) : HttpClient {

    override fun <T : Any> get(url: String): T? = execute(url)
    
    private inline fun <reified T: Any> execute(url: String): T? {
        val request = Request.Builder().url(url).get().build()
        client.newCall(request).execute().use {
            return it.body?.parseBodySuccess()
        }
    }

    private inline fun <reified T: Any> ResponseBody?.parseBody(): T? {
        val type = objectMapper.typeFactory.constructType(T::class.java)
        return this?.let { objectMapper.readValue(it.string(), type) }
    }

}

Now, I would like to be able to call such GET method in this way:

data class MyEntity(...)

class MyService @Inject constructor(private val client: HttpClient) {
    fun performGet(url: String): MyEntity? = client.get<MyEntity>(url)
}

However this is not allowed and the compiler throws an error referring to the line of code

override fun <T : Any> get(endpoint: String): T? = execute(endpoint)

flagging that : Cannot use 'T' as reified type parameter. Use a class instead.

I have been trying to re-write the line as

override inline fun <reified T : Any> get(endpoint: String): T? = execute(endpoint)

however, despite having to make the other two inline functions "non private" the compiler still won't compile because in this last way of writing the overriding function, it says:

Override by a function with reified type parameter

How can I achieve such generic function?

CodePudding user response:

I ended up doing something like this:

interface HttpClient {
    fun <T: Any> get(url: String, type: Class<T>): T?
}

implemented as:

class HttpClientImpl @Inject constructor(...) : HttpClient {

    override fun <T : Any> get(url: String, type: Class<T>): T? = execute(url, type)
    
    private fun <T: Any> execute(url: String, type: Class<T>): T? {
        val request = Request.Builder().url(url).get().build()
        client.newCall(request).execute().use {
            return it.body?.parseBody(type)
        }
    }

    private fun <T: Any> ResponseBody?.parseBody(type: Class<T>): T? {
        val dataType = objectMapper.typeFactory.constructType(type)
        return this?.let { objectMapper.readValue(it.string(), dataType) }
    }

}

that I can call in this way:

data class MyEntity(...)

class MyService @Inject constructor(private val client: HttpClient) {
    fun performGet(url: String): MyEntity? = client.get(url, MyEntity::class.java)
}

I would have preferred to pass the Type directly as an actual type like

client.get<MyEntity>(url)

rather than passing the Class as a parameter, however, just for now it works...

If anyone can suggest a better way of doing this, please let me know.

Updated

As suggested by Pawel, I have created an extra inline extension function to the HttpClient interface

inline fun <reified T:Any> HttpClient.get (url: String) = get(url, T::class.java)

And I'm now able to call the function the way I wanted.

  • Related