Skip to content

Instantly share code, notes, and snippets.

@naturalwarren
Last active March 16, 2022 04:58
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save naturalwarren/56b54759b0f690622938caa91f037be6 to your computer and use it in GitHub Desktop.
Save naturalwarren/56b54759b0f690622938caa91f037be6 to your computer and use it in GitHub Desktop.
A Kotlin-esque API for Retrofit.
/**
* Copyright 2019 Coinbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.coinbase.network.adapter
import com.squareup.moshi.Types
import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
* A [CallAdapter.Factory] which allows [NetworkResponse] objects to be returned from RxJava
* streams.
*
* Adding this class to [Retrofit] allows you to return [Observable], [Flowable], [Single], or
* [Maybe] types parameterized with [NetworkResponse] from service methods.
*
* Note: This adapter must be registered before an adapter that is capable of adapting RxJava
* streams.
*/
class KotlinRxJava2CallAdapterFactory private constructor() : CallAdapter.Factory() {
companion object {
@JvmStatic
fun create() = KotlinRxJava2CallAdapterFactory()
}
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
val rawType = getRawType(returnType)
val isFlowable = rawType === Flowable::class.java
val isSingle = rawType === Single::class.java
val isMaybe = rawType === Maybe::class.java
if (rawType !== Observable::class.java && !isFlowable && !isSingle && !isMaybe) {
return null
}
if (returnType !is ParameterizedType) {
throw IllegalStateException(
"${rawType.simpleName} return type must be parameterized as " +
"${rawType.simpleName}<Foo> or ${rawType.simpleName}<? extends Foo>"
)
}
val observableEmissionType = getParameterUpperBound(0, returnType)
if (getRawType(observableEmissionType) != NetworkResponse::class.java) {
return null
}
if (observableEmissionType !is ParameterizedType) {
throw IllegalStateException(
"NetworkResponse must be parameterized as NetworkResponse<SuccessBody, ErrorBody>"
)
}
val successBodyType = getParameterUpperBound(0, observableEmissionType)
val delegateType = Types.newParameterizedType(
Observable::class.java,
successBodyType
)
val delegateAdapter = retrofit.nextCallAdapter(
this,
delegateType,
annotations
)
val errorBodyType = getParameterUpperBound(1, observableEmissionType)
val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(
null,
errorBodyType,
annotations
)
@Suppress("UNCHECKED_CAST") // Type of delegateAdapter is not known at compile time.
return KotlinRxJava2CallAdapter(
successBodyType,
delegateAdapter as CallAdapter<Any, Observable<Any>>,
errorBodyConverter,
isFlowable,
isSingle,
isMaybe
)
}
}
/**
* Copyright 2019 Coinbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.coinbase.network.adapter
import java.io.IOException
/**
* Represents the result of making a network request.
*
* @param T success body type for 2xx response.
* @param U error body type for non-2xx response.
*/
sealed class NetworkResponse<out T : Any, out U : Any> {
/**
* A request that resulted in a response with a 2xx status code that has a body.
*/
data class Success<T : Any>(val body: T) : NetworkResponse<T, Nothing>()
/**
* A request that resulted in a response with a non-2xx status code.
*/
data class ServerError<U : Any>(val body: U?, val code: Int) : NetworkResponse<Nothing, U>()
/**
* A request that didn't result in a response.
*/
data class NetworkError(val error: IOException) : NetworkResponse<Nothing, Nothing>()
}
/**
* Copyright 2019 Coinbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.coinbase.network.adapter
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
import io.reactivex.functions.Function
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Converter
import retrofit2.HttpException
import java.io.IOException
import java.lang.reflect.Type
internal class KotlinRxJava2CallAdapter<T : Any, U : Any>(
private val successBodyType: Type,
private val delegateAdapter: CallAdapter<T, Observable<T>>,
private val errorConverter: Converter<ResponseBody, U>,
private val isFlowable: Boolean,
private val isSingle: Boolean,
private val isMaybe: Boolean
) : CallAdapter<T, Any> {
override fun adapt(call: Call<T>): Any =
delegateAdapter.adapt(call)
.flatMap {
Observable.just<NetworkResponse<T, U>>(NetworkResponse.Success(it))
}
.onErrorResumeNext(
Function<Throwable, Observable<NetworkResponse<T, U>>> { throwable ->
when (throwable) {
is HttpException -> {
val error = throwable.response().errorBody()
val errorBody = when {
error == null -> null
error.contentLength() == 0L -> null
else -> {
try {
errorConverter.convert(error)
} catch (e: Exception) {
return@Function Observable.just(
NetworkResponse.NetworkError(
IOException(
"Couldn't deserialize error body: ${error.string()}",
e
)
)
)
}
}
}
val serverError = NetworkResponse.ServerError(
errorBody,
throwable.response().code()
)
Observable.just(serverError)
}
is IOException -> {
Observable.just(
NetworkResponse.NetworkError(
throwable
)
)
}
else -> {
throw throwable
}
}
}).run {
when {
isFlowable -> this.toFlowable(BackpressureStrategy.LATEST)
isSingle -> this.singleOrError()
isMaybe -> this.singleElement()
else -> this
}
}
override fun responseType(): Type = successBodyType
}
@Drjacky
Copy link

Drjacky commented Apr 2, 2021

What's the alternative solution if I'm not using Moshi, for this part:

val delegateType = Types.newParameterizedType(
            Observable::class.java,
            successBodyType
        )

If I use this
val delegateTypeee = observableeEmissionType.actualTypeArguments[0],
I get an error:

IllegalArgumentEXCEPTION: Unable to Create call adapter for Single<NetworkResponse<MyModel, ErrorModel>>

@Drjacky
Copy link

Drjacky commented Apr 4, 2021

Solution:

val delegateType = TypeToken.getParameterized(
            Observable::class.java,
            successBodyType
        )

@gallosalocin
Copy link

Solution:

val delegateType = TypeToken.getParameterized(
            Observable::class.java,
            successBodyType
        )

Hi Drjacky, I try this but I got an error in :
val delegateAdapter = retrofit.nextCallAdapter(
this,
delegateType, // required Type! found TypeToken<*>!
annotations
)

Thanks

@Drjacky
Copy link

Drjacky commented Apr 19, 2021

Solution:

val delegateType = TypeToken.getParameterized(
            Observable::class.java,
            successBodyType
        )

Hi Drjacky, I try this but I got an error in :
val delegateAdapter = retrofit.nextCallAdapter(
this,
delegateType, // required Type! found TypeToken<*>!
annotations
)

Thanks

val delegateAdapter = retrofit.nextCallAdapter(
            this,
            delegateType.type,
            annotations
        )

@gallosalocin
Copy link

Nice. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment