On Square.Retrofit, how setup the CallAdapter.Factory so that Retrofit use my CallAdapter<T, Foo<Bar<T>>>

Viewed 410

My final goal here is to have the following retrofit interface declared:

interface SomeService {
  @GET("/path")
  fun getSomethingFlow() : Flow<DataState<SomeApiDataClass>>
}

data class DataState<T>(val data : T, state: State)
// the real data class is a big bigger, but that's the idea
// state is an enum

The intention is that my CallAdapter will handle the Flow and the DataState and let Moshi handle the T.

I've been following mostly from here https://github.com/MohammadSianaki/Retrofit2-Flow-Call-Adapter/tree/master/FlowAdapter/src/main/java/me/sianaki/flowretrofitadapter

So I created a CallAdapter<T, Flow<DataState<T>>> with override fun adapt(call: Call<T>): Flow<DataState<T>> that does all the processing I want it to do.

I've created the CallAdapter.Factory() and all the API data classes have Moshi annotation @JsonClass(generateAdapter = true).

here is the CallAdapter.Factory:

    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (getRawType(returnType) != Flow::class.java) {
            return null
        }
        if (returnType !is ParameterizedType) {
            return null
        }
        val flowType = getParameterUpperBound(0, returnType)
        val rawFlowType = getRawType(flowType)
        if (rawFlowType != DataState::class.java) {
            return null
        }

        return MyCallAdapter(flowType)
    }

and relevant CallAdapter code:


    override fun adapt(call: Call<T>): Flow<DataState<T>> {
        Log.d("TEST", "adapt $call")
        // do all my logic here.

        This method is never called!!!

        return dataState
    }

    override fun responseType(): Type {
        Log.d("TEST", "Retrofit is asking us the type")
        return returnType
    }

At the end I got my Retrofit building like:

        val api = Retrofit.Builder()
            .addConverterFactory(MoshiConverterFactory.create())
            .addCallAdapterFactory(MyAdapterFactory())
            .client(client)
            .baseUrl("https://.... ")
            .build()
            .create(SomeService::class.java)

When I try to call the API I get the following crash:

    java.lang.IllegalArgumentException: Unable to create converter for com.package.DataState<com.package.SomeServiceDataClass>
        for method SomeService.getSomethingFlow
        at retrofit2.Utils.methodError(Utils.java:54)
        at retrofit2.HttpServiceMethod.createResponseConverter(HttpServiceMethod.java:126)
        at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:85)
        at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:39)
.....
     Caused by: java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.package.DataState Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapterFactory from the moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.
        at com.squareup.moshi.ClassJsonAdapter$1.create(ClassJsonAdapter.java:97)
        at com.squareup.moshi.Moshi.adapter(Moshi.java:145)
        at com.squareup.moshi.Moshi.adapter(Moshi.java:105)

It seems that even thou I gave to Retrofit a CallAdapter from Call<T> to Flow<DataState<T>>, it still trying to use data converters (moshi) for parsing the DataState.

On debugging I found that the Factory passes all the checks and returns a new MyCallAdapter and that Retrofit calls my MyCallAdapter.responseType(). So it's mostly there.

The fun adapt(call: Call<T>): Flow<DataState<T>> is never called!

So the question is:

How do I tell retrofit to only use Moshi for T and leave the DataState to me?

1 Answers

After crying a bit and re-considering some life choices, I found the solution.

It is a good ol' case of "should have better read the docs".
Check it out!!

Retrofit Docs

Returns the value type that this adapter uses when converting the HTTP response body to a Java object. For example, the response type for Call is Repo. This type is used to prepare the call passed to #adapt. Note: This is typically not the same type as the returnType provided to this call adapter's factory.

So after updating the Factory this following snippet, it all works.

    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        val typeOfFlow = getInnerType(returnType, Flow::class.java) ?: return null
        val typeOfDataState = getInnerType(typeOfFlow, DataState::class.java)?: return null

        return MyAdapter(typeOfDataState)
    }


  private fun getInnerType(type: Type, klazz: Class<*>): Type? {
        if (type !is ParameterizedType) {
            return null
        }
        if (getRawType(type) != klazz) {
            return null
        }
        return getParameterUpperBound(0, type)
    }
Related